• Linux下的系统编程——线程同步(十三)


    前言:

    多线程编程中,如果多个线程同时访问和修改共享资源,可能会产生竞争条件和数据不一致的问题。同步机制用于协调线程之间的访问和操作,确保数据的正确性和一致性。为了避免多个线程同时访问和操作共享资源导致的问题,可以使用互斥锁(mutex)来实现线程的互斥访问。互斥锁可以保证同一时间只有一个线程访问共享资源、条件变量用于线程之间的通信和同步。一个线程可以等待某个条件成立,而其他线程可以通过发送信号来改变条件变量的状态,从而唤醒等待的线程。读写锁是一种特殊的锁,用于控制对共享资源的读取和写入。多个线程可以同时进行读操作,但只能有一个线程进行写操作,以确保数据的一致性。

    目录

    一、同步概念:

    1.线程同步:

    2.数据混乱原因:

    二、互斥量 mutex

    1.线程同步与锁:

    2.锁的使用注意事项

     3.借助互斥锁管理共享数据实现同步

    (1)不加锁:

    (2)加mutex:

     1)使用mutex(互斥量、互斥锁)一般步骤:

     2)初始化互斥量:

    3)*注意事项:

    (3)加锁步骤测试:

    三、读写锁:

    1.读写锁函数原型:

    2.读写锁原理:

    四、**死锁:

     五、条件变量

        1.初始化条件变量:

        2.阻塞等待条件:

    3.能够借助条件变量,完成生成者消费者

    (1)模型分析

    (2)代码实现

    (3)运行效果​编辑

    (4)一个生产者,多个消费者

    4.条件变量的优点:

    六、信号量: 

    1.基本操作

    2.生产者消费者信号量模型


    一、同步概念:

            

            所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式略有不同。如,设备同步,是指在两 个设备之间规定一个共同的时间参考;数据库同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持 一致;文件同步,是指让两个或多个文件夹里的文件保持一致。等等

            而,编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、互相 配合。主旨在协同步调,按预定的先后次序运行

    1.线程同步:

    协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误

    因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步

    2.数据混乱原因:

    1. 资源共享(独享资源则不会)

    2. 调度随机(意味着数据访问会出现竞争)

    3. 线程间缺乏必要的同步机制

             以上 3 点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。 只要存在竞争关系,数据就很容易出现混乱。

    二、互斥量 mutex

    Linux 中提供一把互斥锁 mutex(也称之为互斥量)。

    每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁

    资源还是共享的,线程间也还是竞争的, 但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。

    1.线程同步与锁:

    “与时间有关的错误”(time related)。为了避免这种数据混乱,线程需要同步。

    2.锁的使用注意事项

    建议锁!对公共数据进行保护。所有线程【应该】在访问公共数据前先拿锁再访问。但锁本身不具备强制性。

     3.借助互斥锁管理共享数据实现同步

            (1)不加锁:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. void *tfn(void *arg)
    7. {
    8. srand(time(NULL));
    9. while (1) {
    10. printf("hello ");
    11. sleep(rand() % 3); //模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
    12. printf("world\n");
    13. sleep(rand() % 3);
    14. }
    15. return NULL;
    16. }
    17. int main(void)
    18. {
    19. pthread_t tid;
    20. srand(time(NULL));
    21. pthread_create(&tid, NULL, tfn, NULL);
    22. while (1) {
    23. printf("HELLO ");
    24. sleep(rand() % 3);
    25. printf("WORLD\n");
    26. sleep(rand() % 3);
    27. }
    28. pthread_join(tid, NULL);
    29. return 0;
    30. }
    31. /*线程之间共享资源stdout*/

     子父线程相互争夺cpu,出现数据混淆现象:

    (2)加mutex:

        1)使用mutex(互斥量、互斥锁)一般步骤

        pthread_mutex_t 类型。(本质是结构体) 

        1. pthread_mutex_t lock;  创建锁

        2  pthread_mutex_init; 初始化        1

        3. pthread_mutex_lock;加锁           1--    --> 0

        4. 访问共享数据(stdout)        

        5. pthrad_mutext_unlock();解锁       0++    --> 1

        6. pthead_mutex_destroy销毁锁


     

     2)初始化互斥量:

            pthread_mutex_t mutex;

            1. pthread_mutex_init(&mutex, NULL);               动态初始化。

            2. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;    静态初始化。

    3)*注意事项:

            尽量保证锁的粒度越小越好。(访问共享数据前加锁。访问结束【立即】解锁。)

            互斥锁,本质是结构体。 我们可以看成整数。 初值为 1。(pthread_mutex_init() 函数调用成功。)

            加锁:--操作, 阻塞线程。

            解锁:++操作, 换醒阻塞在锁上的线程。

            try锁: 尝试加锁,成功--。失败,返回。同时设置错误号 EBUSY

            restrict关键字: 用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成

    (3)加锁步骤测试:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. pthread_mutex_t mutex; //定义一把互斥锁,可以想象为一个int
    7. void *tfn(void *arg)
    8. {
    9. srand(time(NULL));
    10. while (1) {
    11. pthread_mutex_lock(&mutex); //加锁 可以想象成锁-- (1------- -->0)
    12. printf("hello ");
    13. sleep(rand() % 3); //模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
    14. printf("world\n");
    15. pthread_mutex_unlock(&mutex); //解锁 可以想象为锁++ (0------- -->1)
    16. sleep(rand() % 3);
    17. }
    18. return NULL;
    19. }
    20. int main(void)
    21. {
    22. pthread_t tid;
    23. srand(time(NULL));
    24. int ret = pthread_mutex_init(&mutex,NULL); //初始化互斥锁 可以认为锁的值为1
    25. if(ret != 0){
    26. fprintf(stderr,"mutex init error: %s\n",strerror(ret));
    27. exit(1);
    28. }
    29. pthread_create(&tid, NULL, tfn, NULL);
    30. while (1) {
    31. pthread_mutex_lock(&mutex); //加锁 可以想象成锁-- (1------- -->0)
    32. printf("HELLO ");
    33. sleep(rand() % 3);
    34. printf("WORLD\n");
    35. pthread_mutex_unlock(&mutex); //解锁 可以想象为锁++ (0------- -->1)
    36. sleep(rand() % 3);
    37. }
    38. pthread_join(tid, NULL);
    39. pthread_mutex_destroy(&mutex); //销毁互斥锁
    40. return 0;
    41. }

    三、读写锁:

    1.读写锁函数原型:

        锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。

        读共享,写独占

        写锁优先级高

        相较于互斥量而言当读线程多的时候,提高访问效率

        pthread_rwlock_t  rwlock;

        pthread_rwlock_init(&rwlock, NULL);                //初始化读写锁

        pthread_rwlock_rdlock(&rwlock);        try         //读模式加锁

        pthread_rwlock_wrlock(&rwlock);        try         //写模式加锁

        pthread_rwlock_unlock(&rwlock);                     //解锁

        pthread_rwlock_destroy(&rwlock);·                  //销毁读写锁

    2.读写锁原理:

      同时有多个线程对同一全局数据读、写操作

    1. /* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */
    2. #include
    3. #include
    4. #include
    5. int counter; //全局资源
    6. pthread_rwlock_t rwlock; //全局的读写锁
    7. void *th_write(void *arg)
    8. {
    9. int t;
    10. int i = (int)arg;
    11. while (1) {
    12. t = counter; // 保存写之前的值
    13. usleep(1000);
    14. pthread_rwlock_wrlock(&rwlock); //以写模式加锁,写独占
    15. printf("=====write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
    16. pthread_rwlock_unlock(&rwlock); //解锁
    17. usleep(9000); // 给 r 锁提供机会
    18. }
    19. return NULL;
    20. }
    21. void *th_read(void *arg)
    22. {
    23. int i = (int)arg;
    24. while (1) {
    25. pthread_rwlock_rdlock(&rwlock); //读线程间,读锁共享
    26. printf("-----------------read %d: %lu: %d\n", i, pthread_self(), counter);
    27. pthread_rwlock_unlock(&rwlock); //解锁
    28. usleep(2000); // 给写锁提供机会
    29. }
    30. return NULL;
    31. }
    32. int main(void)
    33. {
    34. int i;
    35. pthread_t tid[8]; //设置一个8个线程的数组
    36. pthread_rwlock_init(&rwlock, NULL); //自定义读写锁
    37. for (i = 0; i < 3; i++)
    38. pthread_create(&tid[i], NULL, th_write, (void *)i); //创建3个写线程
    39. for (i = 0; i < 5; i++)
    40. pthread_create(&tid[i+3], NULL, th_read, (void *)i); //创建5个读线程
    41. for (i = 0; i < 8; i++)
    42. pthread_join(tid[i], NULL); //回收8个线程
    43. pthread_rwlock_destroy(&rwlock); //释放读写琐
    44. return 0;
    45. }

    四、**死锁:

    1.发生死锁原因:

    使用锁不恰当导致的现象

            1. 对一个锁反复lock

            2. 两个线程,各自持有一把锁,请求另一把。

    2.避免死锁方法: 

            1.保证资源获取顺序,要求每个线程获取资源的顺序一致

    ·        2.当得不到所有所需资源时,放弃已经获得的资源,等待

     五、*条件变量

        本身不是锁!  但是通常结合锁来使用。 mutex

        pthread_cond_t cond;

        1.初始化条件变量:

            1. pthread_cond_init(&cond, NULL);               动态初始化。

            2. pthread_cond_t cond = PTHREAD_COND_INITIALIZER;    静态初始化。

        2.阻塞等待条件:

            pthread_cond_wait(&cond, &mutex);

            作用:  

                1) 阻塞等待条件变量满足

                2) 解锁已经加锁成功的信号量 (相当于 pthread_mutex_unlock(&mutex))

                3)  当条件满足,函数返回时,重新加锁信号量 (相当于, pthread_mutex_lock(&mutex);)

                1) 和 2)俩步为一个原子操作
        

        pthread_cond_signal():   唤醒阻塞在条件变量上的 (至少)一个线程

        pthread_cond_broadcast(): 唤醒阻塞在条件变量上的所有线程


    3.能够借助条件变量,完成生成者消费者

    (1)模型分析

    (2)代码实现

    1. /*借助条件变量模拟 生产者-消费者 问题*/
    2. #include
    3. #include
    4. #include
    5. #include
    6. /*链表作为公享数据,需被互斥量保护*/
    7. struct msg {
    8. struct msg *next;
    9. int num;
    10. };
    11. struct msg *head;
    12. /* 静态初始化 一个条件变量 和 一个互斥量*/
    13. pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
    14. pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    15. void *consumer(void *p)//消费者函数
    16. {
    17. struct msg *mp;
    18. for (;;) {
    19. pthread_mutex_lock(&lock);
    20. while (head == NULL) { //头指针为空,说明没有节点 可以为if吗
    21. pthread_cond_wait(&has_product, &lock);//发生阻塞
    22. }
    23. mp = head;
    24. head = mp->next; //模拟消费掉一个产品
    25. pthread_mutex_unlock(&lock);
    26. printf("-Consume %lu---%d\n", pthread_self(), mp->num);
    27. free(mp);
    28. sleep(rand() % 5);
    29. }
    30. }
    31. void *producer(void *p)//生产者函数
    32. {
    33. struct msg *mp;
    34. for (;;) {
    35. mp = malloc(sizeof(struct msg));
    36. mp->num = rand() % 1000 + 1; //模拟生产一个产品
    37. printf("-Produce ---------------------%d\n", mp->num);
    38. pthread_mutex_lock(&lock);
    39. mp->next = head;
    40. head = mp;
    41. pthread_mutex_unlock(&lock);
    42. pthread_cond_signal(&has_product); //将等待在该条件变量上的 一个线程唤醒
    43. sleep(rand() % 5);
    44. }
    45. }
    46. int main(int argc, char *argv[])
    47. {
    48. pthread_t pid, cid;
    49. srand(time(NULL));
    50. pthread_create(&pid, NULL, producer, NULL);//创建生产者
    51. pthread_create(&cid, NULL, consumer, NULL);//创建消费者
    52. pthread_join(pid, NULL);//回收生产者
    53. pthread_join(cid, NULL);//回收消费者
    54. return 0;
    55. }

    (3)运行效果

    (4)一个生产者,多个消费者

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. void err_thread(int ret, char *str)
    8. {
    9. if (ret != 0) {
    10. fprintf(stderr, "%s:%s\n", str, strerror(ret));
    11. pthread_exit(NULL);
    12. }
    13. }
    14. struct msg {
    15. int num;
    16. struct msg *next;
    17. };
    18. struct msg *head;
    19. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义/初始化一个互斥量
    20. pthread_cond_t has_data = PTHREAD_COND_INITIALIZER; // 定义/初始化一个条件变量
    21. void *produser(void *arg)
    22. {
    23. while (1) {
    24. struct msg *mp = malloc(sizeof(struct msg));
    25. mp->num = rand() % 1000 + 1; // 模拟生产一个数据`
    26. printf("--produce %d\n", mp->num);
    27. pthread_mutex_lock(&mutex); // 加锁 互斥量
    28. mp->next = head; // 写公共区域
    29. head = mp;
    30. pthread_mutex_unlock(&mutex); // 解锁 互斥量
    31. pthread_cond_signal(&has_data); // 唤醒阻塞在条件变量 has_data上的线程.
    32. sleep(rand() % 3);
    33. }
    34. return NULL;
    35. }
    36. void *consumer(void *arg)
    37. {
    38. while (1) {
    39. struct msg *mp;
    40. pthread_mutex_lock(&mutex); // 加锁 互斥量
    41. while (head == NULL) {
    42. pthread_cond_wait(&has_data, &mutex); // 阻塞等待条件变量, 解锁
    43. } // pthread_cond_wait 返回时, 重新加锁 mutex
    44. mp = head;
    45. head = mp->next;
    46. pthread_mutex_unlock(&mutex); // 解锁 互斥量
    47. printf("---------consumer id: %lu :%d\n", pthread_self(), mp->num);
    48. free(mp);
    49. sleep(rand()%3);
    50. }
    51. return NULL;
    52. }
    53. int main(int argc, char *argv[])
    54. {
    55. int ret;
    56. pthread_t pid, cid;
    57. srand(time(NULL));
    58. ret = pthread_create(&pid, NULL, produser, NULL); // 生产者
    59. if (ret != 0)
    60. err_thread(ret, "pthread_create produser error");
    61. ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者
    62. if (ret != 0)
    63. err_thread(ret, "pthread_create consuer error");
    64. ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者
    65. if (ret != 0)
    66. err_thread(ret, "pthread_create consuer error");
    67. ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者
    68. if (ret != 0)
    69. err_thread(ret, "pthread_create consuer error");
    70. pthread_join(pid, NULL);
    71. pthread_join(cid, NULL);
    72. return 0;
    73. }

    4.条件变量的优点:

    相较于 mutex 而言,条件变量可以减少竞争。

    如直接使用 mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚 (链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引 起消费者之间的竞争。提高了程序效率

    六、信号量: 

    1.基本操作

        应用于线程、进程间同步。

        相当于 初始化值为 N 的互斥量。  N值,表示可以同时访问共享数据区的线程数

        函数:
            sem_t sem;    定义类型。

            int sem_init(sem_t *sem, int pshared, unsigned int value);//初始化信号量

            参数:
                sem: 信号量 

                pshared:    0: 用于线程间同步
                        
                                     1: 用于进程间同步

                value:        N值:指定同时访问的线程数


           sem_destroy();        //销毁信号量

            sem_wait();        一次调用,做一次-- 操作, 当信号量的值为 0 时,再次 -- 就会阻塞。 (对比 pthread_mutex_lock//加锁

            sem_post();        一次调用,做一次++ 操作. 当信号量的值为 N 时, 再次 ++ 就会阻塞。(对比 pthread_mutex_unlock//解锁

    注意:信号量的初值,决定了占用信号量的线程个数

    2.生产者消费者信号量模型

    使用信号量完成线程间同步,模拟生产者,消费者问题

    1. /*信号量实现 生产者 消费者问题*/
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #define NUM 5
    8. int queue[NUM]; //全局数组实现环形队列
    9. sem_t blank_number, product_number; //空格子信号量, 产品信号量
    10. void *producer(void *arg)
    11. {
    12. int i = 0;
    13. while (1) {
    14. sem_wait(&blank_number); //生产者将空格子数--,为0则阻塞等待
    15. queue[i] = rand() % 1000 + 1; //生产一个产品
    16. printf("----Produce---%d\n", queue[i]);
    17. sem_post(&product_number); //将产品数++
    18. i = (i+1) % NUM; //借助下标实现环形
    19. sleep(rand()%1);
    20. }
    21. }
    22. void *consumer(void *arg)
    23. {
    24. int i = 0;
    25. while (1) {
    26. sem_wait(&product_number); //消费者将产品数--,为0则阻塞等待
    27. printf("-Consume---%d\n", queue[i]);
    28. queue[i] = 0; //消费一个产品
    29. sem_post(&blank_number); //消费掉以后,将空格子数++
    30. i = (i+1) % NUM;
    31. sleep(rand()%3);
    32. }
    33. }
    34. int main(int argc, char *argv[])
    35. {
    36. pthread_t pid, cid;
    37. sem_init(&blank_number, 0, NUM); //初始化空格子信号量为5, 线程间共享 -- 0
    38. sem_init(&product_number, 0, 0); //产品数为0
    39. pthread_create(&pid, NULL, producer, NULL); //产生生产者
    40. pthread_create(&cid, NULL, consumer, NULL); //产生消费者
    41. pthread_join(pid, NULL);
    42. pthread_join(cid, NULL);
    43. sem_destroy(&blank_number); //回收生产者
    44. sem_destroy(&product_number); //回收消费者
    45. return 0;
    46. }

     

  • 相关阅读:
    PHP 程序员转 Go 语言的经历分享
    断点是什么,断点有哪几种类型?
    Cholesterol-PEG-DBCO,CLS-PEG-DBCO,胆固醇-聚乙二醇-二苯基环辛炔科研试剂
    pinctrl
    状态机高阶讲解-16
    python开发之个微机器人的二次开发
    设计模式之抽象工厂模式
    Python自动化测试框架之unittest使用详解!
    Python机器学习零基础理解线性回归分析
    每日一题leetcode--删除并获得点数(DP)
  • 原文地址:https://blog.csdn.net/m0_63168877/article/details/132949584