• Linux 线程同步、互斥锁、避免死锁、条件变量


    1. 线程同步概述

    线程同步定义

    线程同步,指的是控制多线程间的相对执行顺序,从而在线程间正确、有序地共享数据,以下为线程同步常见使用场合。

    • 多线程执行的任务在顺序上存在依赖关系
    • 线程间共享数据只能同时被一个线程使用

    线程同步方法

    在实际项目中,经常使用的线程同步方法主要分为三种:

    • 互斥锁
    • 条件变量
    • Posix信号量(包括有名信号量和无名信号量)

    本节内容只介绍互斥锁和条件变量,Posix信号量后续在Posix IPC专题中介绍。

    2. 互斥锁

    互斥锁概念

    互斥锁用于确保同一时间只有一个线程访问共享数据,使用方法为:

    • 加锁
    • 访问共享数据
    • 解锁

    对互斥锁加锁后,任何其他试图再次对其加锁的线程都会被阻塞,直到当前线程释放该互斥锁,解锁时所有阻塞线程都会变成可运行状态,但究竟哪个先运行,这一点是不确定的。

    互斥锁基本API

    初始化与销毁

    互斥锁是用pthread_mutex_t数据类型表示的,在使用互斥锁之前,需要先进行初始化,初始化方法有两种:

    • 设置为常量PTHREAD_MUTEX_INITIALIZER,只适用于静态分配的互斥锁
    • 调用pthread_mutex_init函数,静态分配和动态分配的互斥锁都可以

    互斥锁使用完以后,可以调用pthread_mutex_destroy进行销毁,尤其是对于动态分配的互斥锁,在释放内存前,调用pthread_mutex_destroy是必须的。

    1. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    2. //两个函数的返回值:成功返回0,失败返回错误编号
    3. int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    4. int pthread_mutex_destroy(pthread_mutex_t *mutex);

    其中,pthread_mutex_init的第二个参数attr用于设置互斥锁的属性,如果要使用默认属性,只需把attr设为NULL。

    上锁与解锁

    1. //两个函数的返回值:成功返回0,失败返回错误编号
    2. int pthread_mutex_lock(pthread_mutex_t *mutex);
    3. int pthread_mutex_unlock(pthread_mutex_t *mutex);

    对互斥锁上锁,需要调用pthread_mutex_lock,如果互斥锁已经上锁,调用线程将阻塞到该互斥锁被释放。
    对互斥锁解锁,需要调用pthread_mutex_unlock

    两个特殊的上锁函数

    尝试上锁

    1. //成功返回0,失败返回错误编号
    2. int pthread_mutex_trylock(pthread_mutex_t *mutex);

    如果不希望调用线程阻塞,可以使用pthread_mutex_trylock尝试上锁:

    • 若mutex未上锁,pthread_mutex_trylock将加锁成功,返回0
    • 若mutex已上锁,pthread_mutex_trylock会加锁失败,返回EBUSY

    限时上锁

    1. //成功返回0,失败返回错误编号
    2. int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *time);

    pthread_mutex_timedlock是一个可以设置阻塞时间的上锁函数:

    • 当mutex已上锁时,调用线程会阻塞设定的时间
    • 当达到设定时间时,pthread_mutex_timedlock将加锁失败并解除阻塞,返回ETIMEDOUT

    关于第二个参数time,有两点需要注意:

    • time表示等待的绝对时间,需要将其设为当前时间 + 等待时间
    • time是由struct timespec指定的,它由秒和纳秒来描述时间

    示例代码

    1. /*
    2. * 测试使用上述4个加锁函数
    3. */
    4. #include <pthread.h>
    5. #include <time.h>
    6. #include <errno.h>
    7. #include <stdio.h>
    8. pthread_mutex_t mutex1;
    9. pthread_mutex_t mutex2;
    10. pthread_mutex_t mutex3;
    11. void *thread1_start(void *arg)
    12. {
    13. pthread_mutex_lock(&mutex1);
    14. printf("thread1 has locked mutex1\n");
    15. sleep(2); //保证thread2执行时mutex1还未解锁
    16. pthread_mutex_unlock(&mutex1);
    17. }
    18. void *thread2_start(void *arg)
    19. {
    20. if (pthread_mutex_trylock(&mutex2) == 0)
    21. printf("thread2 trylock mutex2 sucess\n");
    22. if (pthread_mutex_trylock(&mutex1) == EBUSY)
    23. printf("thread2 trylock mutex1 failed\n");
    24. pthread_mutex_unlock(&mutex2);
    25. }
    26. void *thread3_start(void *arg)
    27. {
    28. struct timespec time;
    29. struct tm *tmp_time;
    30. char s[64];
    31. int err;
    32. pthread_mutex_lock(&mutex3);
    33. printf("thread3 has locked mutex3\n");
    34. /*获取当前时间,并转化为本地时间打印*/
    35. clock_gettime(CLOCK_REALTIME, &time);
    36. tmp_time = localtime(&time.tv_sec);
    37. strftime(s, sizeof(s), "%r", tmp_time);
    38. printf("current time is %s\n", s);
    39. /*设置time = 当前时间 + 等待时间10S*/
    40. time.tv_sec = time.tv_sec + 10;
    41. /*mutex3已上锁,这里会阻塞*/
    42. if (pthread_mutex_timedlock(&mutex3, &time) == ETIMEDOUT)
    43. printf("pthread_mutex_timedlock mutex3 timeout\n");
    44. /*再次获取当前时间,并转化为本地时间打印*/
    45. clock_gettime(CLOCK_REALTIME, &time);
    46. tmp_time = localtime(&time.tv_sec);
    47. strftime(s, sizeof(s), "%r", tmp_time);
    48. printf("the time is now %s\n", s);
    49. pthread_mutex_unlock(&mutex3);
    50. }
    51. int main()
    52. {
    53. pthread_t tid1;
    54. pthread_t tid2;
    55. pthread_t tid3;
    56. /*测试pthread_mutex_lock和pthread_mutex_trylock*/
    57. pthread_mutex_init(&mutex1, NULL);
    58. pthread_mutex_init(&mutex2, NULL);
    59. pthread_create(&tid1, NULL, thread1_start, NULL);
    60. pthread_create(&tid2, NULL, thread2_start, NULL);
    61. if (pthread_join(tid1, NULL) == 0)
    62. {
    63. pthread_mutex_destroy(&mutex1);
    64. }
    65. if (pthread_join(tid2, NULL) == 0)
    66. {
    67. pthread_mutex_destroy(&mutex2);
    68. }
    69. /*测试pthread_mutex_timedlock*/
    70. pthread_mutex_init(&mutex3, NULL);
    71. pthread_create(&tid3, NULL, thread3_start, NULL);
    72. if (pthread_join(tid3, NULL) == 0)
    73. {
    74. pthread_mutex_destroy(&mutex3);
    75. }
    76. return 0;
    77. }

    3. 避免死锁

    线程的死锁概念

    线程间死锁,指的是线程间相互等待临界资源而造成彼此无法继续执行的现象。

    产生死锁的四个必要条件

    • 互斥条件:资源同时只能被一个线程使用,此时若有其他线程请求该资源,则请求线程必须等待
    • 不可剥夺条件:线程获得的资源在未使用完毕前,不能被其他线程抢夺,只能由获得该资源的线程主动释放
    • 请求与保持条件:线程已经至少得到了一个资源,但又提出了新的资源请求,而新的资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放
    • 循环等待条件:存在一个资源等待环,环中每一个线程都占有下一个线程所需的至少一个资源

    直观上看,循环等待条件似乎和死锁的定义一样,其实不然,因为死锁定义中的要求更为严格:

    • 循环等待条件要求P(i+1)需要的资源,至少有一个来自P(i)即可
    • 死锁定义要求P(i+1)需要的资源,由且仅由P(i)提供

    如何避免死锁

    • 所有线程以相同顺序加锁
    • 给所有的临界资源分配一个唯一的序号,对应的线程锁也分配同样的序号,系统中的所有线程按照严格递增的次序请求资源
    • 使用pthread_mutex_trylock尝试加锁,若失败就放弃上锁,同时释放已占有的锁
    • 使用pthread_mutex_timedlock限时加锁,若超时就放弃上锁,同时释放已占有的锁

    4. 条件变量

    条件变量概念

    • 条件变量是线程另一种可用的同步机制,它给多线程提供了一个回合的场所
    • 条件变量本身需要由互斥锁保护,线程在改变条件之前必须先上锁,其他线程在获得互斥锁之前不会知道条件发生了改变
    • 条件变量和互斥锁一起使用,可以使线程以无竞争的方式等待特定条件的发生

    条件变量基本API

    初始化与销毁

    条件变量是用pthread_cond_t数据类型表示的,和互斥锁类似,条件变量的初始化方法也有两种:

    • 设置为常量PTHREAD_COND_INITIALIZER,只适用于静态分配的条件变量
    • 调用pthread_cond_init函数,适用于静态分配和动态分配的条件变量

    条件变量使用完以后,可以调用pthread_cond_destroy进行销毁,同样的,如果是动态分配的条件变量,在释放内存前,该操作也是必须的。

    1. pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    2. //两个函数的返回值:成功返回0,失败返回错误编号
    3. int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
    4. int pthread_cond_destroy(pthread_cond_t *cond);

    其中,pthread_cond_init的第二个参数attr用于设置条件变量的属性,如果要使用默认属性,只需把attr设为NULL。

    等待条件满足

    1. //两个函数的返回值:成功返回0,失败返回错误编号
    2. int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
    3. int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *timeout);

    可以调用pthread_cond_wait函数等待条件满足,使用步骤如下,传递给函数的互斥锁对条件进行保护,在条件满足之前,调用线程将一直阻塞。

    • 调用线程将锁住的互斥量传给pthread_cond_wait
    • pthread_cond_wait自动把调用线程放到等待条件的线程列表上,然后对互斥锁解锁
    • 当条件满足,pthread_cond_wait返回时,互斥锁再次被锁住
    • pthread_cond_wait返回后,调用线程再对互斥锁解锁

    pthread_cond_timedwait是一个限时等待条件满足的函数,如果发生超时时条件还没满足,pthread_cond_timedwait将重新对互斥锁上锁,然后返回ETIMEDOUT错误。

    注意:当条件满足从pthread_cond_wait和pthread_cond_timedwait返回时,调用线程必须重新计算条件,因为另一个线程可能已经在运行并改变了条件。

    给线程发信号

    有两个函数可以用于通知线程条件已经满足:

    • pthread_cond_signal至少能唤醒一个等待该条件的线程
    • pthread_cond_broadcast可以唤醒等待该条件的所有线程
    1. //两个函数的返回值:成功返回0,失败返回错误编号
    2. int pthread_cond_signal(pthread_cond_t *cond);
    3. int pthread_cond_broadcast(pthread_cond_t *cond);

    在调用上面两个函数时,我们说这是在给线程发信号,注意,一定要先获取互斥锁,再改变条件,然后给线程发信号,最后再对互斥锁解锁。

    示例代码

    1. /*
    2. * 结合使用条件变量和互斥锁进行线程同步
    3. */
    4. #include <pthread.h>
    5. #include <stdio.h>
    6. static pthread_cond_t cond;
    7. static pthread_mutex_t mutex;
    8. static int cond_value;
    9. static int quit;
    10. void *thread_signal(void *arg)
    11. {
    12. while (!quit)
    13. {
    14. pthread_mutex_lock(&mutex);
    15. cond_value++; //改变条件,使条件满足
    16. pthread_cond_signal(&cond); //给线程发信号
    17. printf("signal send, cond_value: %d\n", cond_value);
    18. pthread_mutex_unlock(&mutex);
    19. sleep(1);
    20. }
    21. }
    22. void *thread_wait(void *arg)
    23. {
    24. while (!quit)
    25. {
    26. pthread_mutex_lock(&mutex);
    27. /*通过while (cond is true)来保证从pthread_cond_wait成功返回时,调用线程会重新检查条件*/
    28. while (cond_value == 0)
    29. pthread_cond_wait(&cond, &mutex);
    30. cond_value--;
    31. printf("signal recv, cond_value: %d\n", cond_value);
    32. pthread_mutex_unlock(&mutex);
    33. sleep(1);
    34. }
    35. }
    36. int main()
    37. {
    38. pthread_t tid1;
    39. pthread_t tid2;
    40. pthread_cond_init(&cond, NULL);
    41. pthread_mutex_init(&mutex, NULL);
    42. pthread_create(&tid1, NULL, thread_signal, NULL);
    43. pthread_create(&tid2, NULL, thread_wait, NULL);
    44. sleep(5);
    45. quit = 1;
    46. pthread_join(tid1, NULL);
    47. pthread_join(tid2, NULL);
    48. pthread_cond_destroy(&cond);
    49. pthread_mutex_destroy(&mutex);
    50. return 0;
    51. }

    转载至:https://zhuanlan.zhihu.com/p/633169684

  • 相关阅读:
    protoc-gen-doc 自定义模板规则详解
    java生成随机数的三种写法
    【知网研学】使用方法
    2022re:Invent:亚马逊云科技拥有强大的云原生数据能力
    【长文档】进行排版的正确顺序?
    A-Level陆续放榜,这些重要事宜需要关注
    C语言练习百题之宏#define命令
    老徐和阿珍的故事:Runnable和Callable有什么不同?
    合工大-人工智能原理实验报告
    英语——记忆篇——谐音法+拼音法
  • 原文地址:https://blog.csdn.net/qq18218628646/article/details/133138410