• 线程同步的实现


    互斥锁

            互斥锁的使用就是当有线程访问进程空间中的公共资源时,线程执行“加锁”操作(将资源锁起来),阻止其它线程的访问。访问完成后,该线程(谁锁上的必须由谁来解锁)负责完成“解锁”操作,将资源让给其它线程。当有多个线程同时需要访问同一个公共资源时,谁先上锁成功,这公共资源就归谁使用,只有等他用完了,释放锁了,其他线程才能用

    互斥锁的使用流程

    (1)定义一个互斥锁

            在<pthread.h>头文件中,有一个专门用来定义互斥锁变量的结构体,叫pthread_mutex_t,我们直接拿来用就是了,使用方法如下

    pthread_mutex_t myMutex;

    (2)初始化互斥锁

            互斥锁的初始化方法有两种,一种是使用特定的宏,一种是使用专门的初始化函数,一般没啥特殊要求我们就用第一种宏就行,对于调用 malloc() 函数分配动态内存的互斥锁,只能使用第二种方式。

    使用宏的方法

    pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;

    使用函数的方法

    pthread_mutex_t myMutex;
    pthread_mutex_init(&myMutex , NULL);

    关于pthread_mutex_init() 函数,他的原型是这样的

    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

    关于他的参数,mutex 参数表示要初始化的互斥锁;attr 参数用于自定义新建互斥锁的属性,当attr 的值为 NULL 时表示以默认属性创建互斥锁,我们一般也就使用NULL。

    (3)加锁

    int pthread_mutex_lock(pthread_mutex_t* mutex);  

    //或者用下面这个

    int pthread_mutex_trylock(pthread_mutex_t* mutex); 

    有关两者的区别,具体如下

    pthread_mutex_lock() 函数会使线程进入等待(阻塞)状态,直至互斥锁得到释放;

    pthread_mutex_trylock() 函数不会阻塞线程,直接返回非零数(表示加锁失败)。

    (4)解锁

    int pthread_mutex_unlock(pthread_mutex_t* mutex); 

    说到最后,用一个实际案例来实验一下,模拟的是网上买票的工作流程。创建了四个子线程,去模拟不同的买票窗口去负责卖票。

    1. #include<stdio.h>
    2. #include<stdlib.h>
    3. #include<pthread.h>
    4. #include<unistd.h>
    5. int ticket_sum = 30;//这表示票的总量
    6. //创建互斥锁变量并初始化
    7. pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;
    8. //模拟售票员卖票
    9. void *sell_ticket(void *arg)
    10. {
    11. //输出当前线程的ID
    12. printf("id = %u\n", pthread_self());
    13. int i;
    14. int islock = 0;
    15. for (i = 0; i < 10; i++)
    16. {
    17. //当前线程“加锁”
    18. islock = pthread_mutex_lock(&myMutex);
    19. //如果“加锁”成功,执行如下代码
    20. if (islock == 0)
    21. {
    22. //如果票数 >0 ,开始卖票
    23. if (ticket_sum > 0)
    24. {
    25. sleep(1);
    26. printf("%u sell the NO.%d ticket\n",pthread_self(),30-ticket_sum+1);
    27. ticket_sum--;
    28. }
    29. pthread_mutex_unlock(&myMutex);
    30. }
    31. }
    32. return 0;
    33. }
    34. int main()
    35. {
    36. int flag;
    37. int i;
    38. //创建4个线程
    39. pthread_t tids[4];
    40. for (i = 0; i < 4; i++)
    41. {
    42. flag = pthread_create(&tids[i], NULL, &sell_ticket, NULL);
    43. if (flag != 0)
    44. {
    45. printf("fail\n");
    46. return 0;
    47. }
    48. }
    49. sleep(30); //等待4个子线程完成
    50. for (i = 0; i < 4; i++)
    51. {
    52. flag = pthread_join(tids[i], NULL);
    53. if (flag != 0)
    54. {
    55. printf("tid=%d wait fail !", tids[i]);
    56. return 0;
    57. }
    58. }
    59. return 0;
    60. }

            程序中pthread_self()这个其实是这样子的,我直接man出来,大家看一下,就是用来返回,调用线程的id的。

            关于这个程序案例,写完后,我自己其实运行了几次,之前我将票的总数量设置成10,发现从头到尾只有一个线程在被调用,我以为是我的程序有问题,在网上查阅了一下资料发现,是我们当前的电脑机子性能太好所致,当操作数量少的情况下的多线程,电脑自己其实就使用一个线程就给我们安排了,于是我加大了数量,这才发生点变化出来

    信号量

            互斥锁的状态值只有开锁和解锁两个方面,信号量则不同,他可以根据实际场景的不同设置自己想要的转态值,可以说是更加灵活吧。

            信号量可以分成是二进制信号量和计数信号量,二进制信号量就可以用来替代互斥锁,就是只将信号量的初始值设置成1,然后只能0,1间变换,可以理解成每次只允许一个线程同时访问一片公共资源,而计数信号量就是将信号量的初始值设置的多一点,大于1的那种,意味着每次可以同时接待多个线程来访问公共资源。

    下面来看看信号量这东西的用法,具体咋用

    sem_t  mySem;        //先定义一个信号量

    //信号量的初始化

    int sem_init(sem_t *sem, int pshared, unsigned int value);

    各个参数的含义分别为:

    1、参数sem:表示要初始化的目标信号量;

    2、参数pshared:表示该信号量是否可以和其他进程共享,pshared 值为 0 时表示不共享,值为 1 时表示共享;

    3、参数value:设置信号量的初始值

    对于初始化好了的信号量,我们可以借助 头文件提供的一些函数操作

    入参都是要操作的信号量

    //将信号量的值“加 1”

    int sem_post(sem_t* sem);        

    //对信号量做“减 1”操作,当信号量的值为 0 时,阻塞当前线程等待

    int sem_wait(sem_t* sem); 

    //当信号量的值为 0 时,sem_trywait() 函数并不会阻塞当前线程,而是立即返回 -1;

    int sem_trywait(sem_t* sem);

    //手动销毁信号量

    int sem_destroy(sem_t* sem);

    当一个线程要用公共资源的时候就先,sem_wait,等待处理完后再sem_post,把位置让出来,就好比银行里的多个服务窗口,你坐那办理业务的时候就需要把可用位置减1,也就是sem_wait,等你办理完后就需要把位置让出来,也就是sem_post。

    条件变量

    条件变量是线程同步的另一种手段,主要逻辑就是等待和唤醒。条件不满足时,线程等待;条件满足时,线程被(其他线程)唤醒。条件变量一般和互斥量一起使用,因为需要保证多线程互斥地修改条件。条件变量阻塞的是线程,不像互斥锁阻塞的是资源。

    读写锁

    读写锁的核心思想是:将线程访问共享数据时发出的请求分为两种,分别是:

    读请求:只读取共享数据,不做任何修改;

    写请求:存在修改共享数据的行为。

    当前读写锁的状态发起读请求发起写请求
    无锁允许允许
    读锁允许阻塞等待
    写锁阻塞等待阻塞等待

    其实核心的要点就是,多个读的进程由于并不改变原内容,所以可以多个并行,但一旦设计到写,那就需要等待一下,让写完了,数据内容不变了再进行操作。


    如何避免死锁 ?

    产生死锁的原因:当进程空间中的某公共资源不允许多个线程同时访问时,某线程访问公共资源后不及时释放资源,就很可能产生线程死锁。

    解决办法

    使用互斥锁的时候,用完后及时地解锁;使用信号量时,用完后及时 sem_post();读写锁也是一样,及时解锁。

    另外,多个线程请求资源的顺序最好保持一致。线程 t1 需要先请求 mutex 锁然后再请求 mutex2 锁,而 t2 需要先请求 mutex2 锁然后再请求 mutex 锁,这是一种典型的因“请求资源顺序不一致”,两个线程之间会相互等待对方解锁而发生死锁。

  • 相关阅读:
    英雄联盟胜负预测--简易肯德基上校
    简述Tomcat的本质
    产品文档-BRD、MRD和PRD
    leetcode 239. Sliding Window Maximum 滑动窗口最大值(困难)
    kotlin基础之协程
    Java_笔记_static_静态变量方法工具类_main方法
    浅谈C++|STL初识篇
    c# ManualResetEvent WaitHandle 实现同步
    附近商户-实现附近商户功能
    刘知远:大模型值得探索的十个研究方向
  • 原文地址:https://blog.csdn.net/qq_45570844/article/details/126839234