• Linux 线程互斥


                        

    在这漫长的暑假,学习让我感到不再孤单,一起为了秋招努力吧!(有一位一起学习的朋友,最近拿到了腾讯的实习,好羡慕~)

    目录

    进程线程间的互斥相关背景概念

    多执行流下没有互斥锁带来的问题 

    线程不安全的原因

    互斥量接口

    pthread_mutex_init

    pthread_mutex_destroy

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

    pthread_mutex_lock

    pthread_mutex_unlock

    互斥锁&抢票逻辑代码

    C++11里面的互斥锁

    锁的原理 

    汇编代码层次理解 

    补充

    静态的互斥锁使用


    进程线程间的互斥相关背景概念

    临界资源:多线程执行流共享的资源就叫做临界资源
    临界区:每个线程内部,访问临界资源的代码,就叫做临界区
    互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
    原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

    是不是感觉太官方了,我们来看一下稍微通俗的解释(方便将来面试来讲):

    多执行流下没有互斥锁带来的问题 

    我们先不使用互斥锁,实现一个抢票逻辑看会出现什么问题。

    代码测试:

    test.cc

    1. #include <iostream>
    2. #include <pthread.h>
    3. #include <unistd.h>
    4. using namespace std;
    5. int tickes = 1000;
    6. void* ThreadRun(void* args)
    7. {
    8. int id = *(int*)args;
    9. delete (int*)args;
    10. while(true)
    11. {
    12. if(tickes > 0)
    13. {
    14. //抢票
    15. usleep(1000);
    16. cout << "我是[" << id <<"] 我要抢的票是: " << tickes << endl;
    17. tickes--;
    18. }
    19. else
    20. {
    21. break;
    22. }
    23. }
    24. }
    25. int main()
    26. {
    27. pthread_t tid[5];
    28. for(int i = 0; i < 5; i++)
    29. {
    30. int* id = new int(i);
    31. pthread_create(tid+i, nullptr, ThreadRun, (void*)id);
    32. }
    33. for(int i = 0; i < 5; i++)
    34. {
    35. pthread_join(tid[i], nullptr);
    36. }
    37. return 0;
    38. }

    Makefile

    1. test:test.cc
    2. g++ -o $@ $^ -lpthread -std=c++11
    3. .PHONY:clean
    4. clean:
    5. rm -f test

    运行结果:

    这时候我们会发现,多个线程在抢票过程中,会抢到负数的情况,这就意味着票卖多了。这种情况是商家不愿意看到的。所以这段代码在多执行流下存在线程安全问题! 

    线程不安全的原因

    实际上ticket--并非原子性的,它实际上要进行计算是要经过多步的,在这期间就有可能被多个线程重入,导致可能出现问题。 

    如何解决上面的问题呢?对临界区进行加锁!

    1、代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

    2、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
    3、如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
    要做到这三点,本质上就是需要一把锁。

    Linux上提供的这把锁叫互斥量。

    互斥量接口

    pthread_mutex_init

    pthread_mutex_t* restrict mutex:传入要初始化锁的地址 

    const pthread_mutexattr_t* restrict attr:我们不用关心,设置成nullptr

    pthread_mutex_destroy

    pthread_mutex_t* mutex:传入要释放锁的地址 

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

    定义在全局的或静态的锁,不用再手动初始化和销毁,直接用就可以了。 

    pthread_mutex_lock

     pthread_mutex_t* mutex:传入锁的地址进行上锁

    pthread_mutex_unlock

    pthread_mutex_t:传入锁的地址,给对应的锁解开 

    互斥锁&抢票逻辑代码

    test.cc

    1. #include <iostream>
    2. #include <pthread.h>
    3. #include <unistd.h>
    4. using namespace std;
    5. class Ticket
    6. {
    7. public:
    8. Ticket()
    9. :_tickes(1000)
    10. {
    11. pthread_mutex_init(&_mtx, nullptr);
    12. }
    13. ~Ticket()
    14. {
    15. pthread_mutex_destroy(&_mtx);
    16. }
    17. bool GetTicket()
    18. {
    19. bool rets = true;//rets是每个线程私有的
    20. //对临界区进行加锁保护
    21. //执行这部分代码的执行流是互斥的,也是串行的
    22. pthread_mutex_lock(&_mtx);
    23. if(_tickes > 0)
    24. {
    25. //抢票
    26. usleep(1000);
    27. cout << "我是[" << pthread_self() << "] 我要抢的票是: " << _tickes << endl;
    28. _tickes--;
    29. }
    30. else
    31. {
    32. //没有票
    33. printf("票已经抢空了\n");
    34. rets = false;
    35. }
    36. pthread_mutex_unlock(&_mtx);
    37. return rets;
    38. }
    39. private:
    40. int _tickes;
    41. pthread_mutex_t _mtx;
    42. };
    43. void* ThreadRun(void* args)
    44. {
    45. Ticket* t = (Ticket*)args;
    46. while(true)
    47. {
    48. if(!t->GetTicket())
    49. {
    50. break;
    51. }
    52. }
    53. }
    54. int main()
    55. {
    56. Ticket* t = new Ticket;
    57. pthread_t tid[5];
    58. for(int i = 0; i < 5; i++)
    59. {
    60. int* id = new int(i);
    61. pthread_create(tid+i, nullptr, ThreadRun, (void*)t);
    62. }
    63. for(int i = 0; i < 5; i++)
    64. {
    65. pthread_join(tid[i], nullptr);
    66. }
    67. return 0;
    68. }

    运行结果(部分):

     这时候我们发现不会出现负数的情况了。

    C++11里面的互斥锁

    运行结果:

     

    锁的原理 

    汇编代码层次理解 

    原子性:一行代码经过编译后,只有一行汇编代码。ticket--经过汇编是有多行代码的,但是互斥量在设计的时候,绝对不是 "--" 设计的,而是使用上面的方法完成原子性的逻辑。

     

           实际上只有互斥还是存在一定的问题,比如一个线程申请锁处理完逻辑释放锁之后,它会再次竞争锁,甚至在其他线程还没唤醒之前又进行申请锁释放锁的逻辑。因为其它在挂起等待的线程被唤醒是有一定代价的,竞争力明显小于刚释放锁的线程,除非它的时间片到了或收到信号,这时候被挂起的线程才有可能竞争到锁。

    补充

    实际上临界区我们加完锁之后,就不用担心重入的问题了。

    静态的互斥锁使用

    test.cc

    1. pthread_mutex_t mymtx = PTHREAD_MUTEX_INITIALIZER;
    2. class Ticket
    3. {
    4. public:
    5. bool GetTicket()
    6. {
    7. bool rets = true;//rets是每个线程私有的
    8. //对临界区进行加锁保护
    9. //执行这部分代码的执行流是互斥的,也是串行的
    10. pthread_mutex_lock(&mymtx);
    11. if(_tickes > 0)
    12. {
    13. //抢票
    14. usleep(1000);
    15. cout << "我是[" << pthread_self() << "] 我要抢的票是: " << _tickes << endl;
    16. _tickes--;
    17. }
    18. else
    19. {
    20. //没有票
    21. printf("票已经抢空了\n");
    22. rets = false;
    23. }
    24. pthread_mutex_unlock(&mymtx);
    25. return rets;
    26. }
    27. private:
    28. int _tickes = 1000;
    29. pthread_mutex_t _mtx;
    30. };
    31. void* ThreadRun(void* args)
    32. {
    33. Ticket* t = (Ticket*)args;
    34. while(true)
    35. {
    36. if(!t->GetTicket())
    37. {
    38. break;
    39. }
    40. }
    41. }
    42. int main()
    43. {
    44. Ticket* t = new Ticket;
    45. pthread_t tid[5];
    46. for(int i = 0; i < 5; i++)
    47. {
    48. int* id = new int(i);
    49. pthread_create(tid+i, nullptr, ThreadRun, (void*)t);
    50. }
    51. for(int i = 0; i < 5; i++)
    52. {
    53. pthread_join(tid[i], nullptr);
    54. }
    55. return 0;
    56. }

    运行结果:

     

     看到这里,给博主点个赞吧~

                      

  • 相关阅读:
    代理模式 静态代理 动态代理
    seq2seq翻译实战-Pytorch复现
    Docker搭建MySQL主从模式案例
    网站whois查询易语言代码
    watch()监听vue2项目角色权限变化更新挂载
    Vue08/Vue 配置路由规则 、Vue声明式导航、声明式导航传参接收两种方式
    面试:Glide和Fresco图片框架对比
    TensorFlow入门(二十、损失函数)
    二维数组的认识和使用
    GPT,GPT-2,GPT-3,InstructGPT的进化之路
  • 原文地址:https://blog.csdn.net/qq_58724706/article/details/125409509