• 线程同步与互斥


    目录

    前言:基于多线程不安全并行抢票

    一、线程互斥锁 mutex

    1.1 加锁解锁处理多线程并发

     1.2 如何看待锁

    1.3 如何理解加锁解锁的本质

    1.4 C++RAII方格设计封装锁

    前言:基于线程安全的不合理竞争资源

    二、线程同步

    1.1 线程同步处理抢票

    1.2 如何理解"条件变量"

    1.3 如何理解条件变量函数需要传锁参数


    前言:基于多线程不安全并行抢票

    1. #include
    2. #include
    3. #include
    4. #define NUM 10
    5. using namespace std;
    6. int global_ticket = 10000;
    7. void* GetTicket(void* args)
    8. {
    9. char* pc =(char*)args;
    10. while(global_ticket > 0)
    11. {
    12. usleep(123);
    13. cout << pc <<" ticket= " << global_ticket--<
    14. }
    15. delete pc;
    16. }
    17. int main()
    18. {
    19. for(int i=0;i
    20. {
    21. char* pc = new char[64];
    22. pthread_t tid;
    23. snprintf(pc,128,"thread %d get a ticket",i+1);
    24. pthread_create(&tid,nullptr,GetTicket,pc);
    25. }
    26. while(true);
    27. return 0;
    28. }

    发现结果和我们代码的预想不一样,出现了票已经售完却仍抢票的现象!

    为什么会出现这种现象? 原来Linux线程是轻量级进程,是CPU调度的基本单位,所以每个线程在CPU中都有自己的时间片,所以线程会在CPU上不断的切换,这样有可能某个线程在运行函数过程中突然被截胡了!然后再轮到它的时候继续运行!那结合本次抢票逻辑,可能某个线程首先while条件满足了拥有了抢票资格,但是没有开始抢票就被截胡了,下次回来后它仍拥有资格拿到一张票,可是之前票已经被抢完了!所以他其实没有资格了,但是因为不安全访问全局变量,使得它可以继续拿到票!

    这样因为线程的特性原因使得我们无法安全的访问一个全局共享变量!所以我们要使线程互斥访问该变量,当某一个线程访问的时候,其他线程无法干预截胡,该线程必须将自己的代码逻辑执行完或者不执行(要么不做,做就必须完成!) 这就叫线程的互斥!那些代码区域称作为临界区!


    一、线程互斥锁 mutex

    1.1 加锁解锁处理多线程并发


    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. using namespace std;
    7. int tickets = 10000;
    8. class Thread_data
    9. {
    10. public:
    11. string name_;
    12. pthread_mutex_t* mutex_;
    13. };
    14. void* pthread_route(void* args)
    15. {
    16. Thread_data* pt = static_cast(args);
    17. while(true)
    18. {
    19. //加锁
    20. pthread_mutex_lock(pt->mutex_);
    21. if(tickets > 0)
    22. {
    23. cout << pt->name_<<"get a ticket, tickets = "<<--tickets <
    24. //解锁
    25. pthread_mutex_unlock(pt->mutex_);
    26. //!!!抢完票后的操作 如果没有这个步骤,该线程会一直占用CPU执行抢票逻辑!
    27. usleep(1234);
    28. }
    29. else
    30. {
    31. //注意这个逻辑里也要解锁!
    32. pthread_mutex_unlock(pt->mutex_);
    33. break;
    34. }
    35. }
    36. delete pt;
    37. }
    38. #define NUM 5
    39. int main()
    40. {
    41. vector<pthread_t> vp(NUM);
    42. char buffer[64];
    43. //锁初始化
    44. //1.锁类型 + 初始化函数
    45. //2.全局变量 pthread_mutex_t mutex =宏(PTHREAD_MUTEX_INITIALIZER)
    46. pthread_mutex_t mutex;
    47. pthread_mutex_init(&mutex,nullptr);
    48. for(int i=0;i
    49. {
    50. pthread_t tid;
    51. snprintf(buffer,sizeof(buffer),"thread %d ",i+1);
    52. Thread_data* pt = new Thread_data();
    53. pt->mutex_ = &mutex;
    54. pt->name_ = buffer;
    55. pthread_create(&(vp[i]),nullptr,pthread_route,pt);
    56. }
    57. for(const auto& tid : vp)
    58. {
    59. pthread_join(tid,nullptr);
    60. }
    61. return 0;
    62. }

    现在结果看到不会出现票数为负数的情况,锁保护了我们的共享资源tickets! 


     1.2 如何看待锁

    a.锁本身被线程共享,是一个共享资源,锁保护全局变量,锁本身也需要被保护

    b.加锁和解锁的过程必须是安全的,也就说加锁的过程必须是原子性的!

    c.如果锁申请成功,就继续向后执行,如果不成功则执行流阻塞!

    d.谁持有锁,谁进入临界区!

    1.3 如何理解加锁解锁的本质

    首先理解这个过程我们首先要明确关于CPU的一个知识就是寄存器:

    寄存器只有一套,但寄存器被线程所共享,寄存器存储的内容只被当前线程所拥有!

    加锁的过程:

    ① 将内存中的线程变量al的值0放入CPU的寄存器内

    ② 然后将寄存器内的0与内存变量mutex值交换,其中mutex的为1,这样mutex = 0

    ③ 最后通过判断al在寄存器的值来判断是否能申请锁成功!>0 成功 else 不成功

    此时当某个线程完成了前面两个步骤,其他线程申请锁时将等于0的mutex与al交换,al依旧是0所以他无法申请锁!这样就可以保证只有一个线程申请到锁!

    解锁的过程:就是把寄存器al的1存入mutex中即可!

    这里swap过程很重要,它是一条汇编完成!其本质是将共享变量放入到我们的上下文中!


    1.4 C++RAII方格设计封装锁

    1. //mutex.hpp
    2. #include
    3. #include
    4. class Mutex
    5. {
    6. public:
    7. Mutex(pthread_mutex_t* lock_p = nullptr):lock_p_(lock_p)
    8. {}
    9. void lock()
    10. {
    11. if(lock_p_)
    12. pthread_mutex_lock(lock_p_);
    13. }
    14. void unlock()
    15. {
    16. if(lock_p_)
    17. pthread_mutex_unlock(lock_p_);
    18. }
    19. private:
    20. pthread_mutex_t* lock_p_;
    21. };
    22. class LockGuard
    23. {
    24. public:
    25. LockGuard(pthread_mutex_t* mutex):mutex_(mutex)
    26. {
    27. mutex_.lock();
    28. }
    29. ~LockGuard()
    30. {
    31. mutex_.unlock();
    32. }
    33. private:
    34. Mutex mutex_;
    35. };

    💡该封装利用局部类变量的构造析构随着作用域自动调用使得加锁解锁自动调用! 


    前言:基于线程安全的不合理竞争资源

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include"mutex.hpp"
    7. using namespace std;
    8. int tickets = 10000;
    9. class Thread_data
    10. {
    11. public:
    12. string name_;
    13. pthread_mutex_t* mutex_;
    14. };
    15. void* pthread_route(void* args)
    16. {
    17. Thread_data* pt = static_cast(args);
    18. while(true)
    19. {
    20. //加锁
    21. //pthread_mutex_lock(pt->mutex_);
    22. LockGuard lock(pt->mutex_);
    23. if(tickets > 0)
    24. {
    25. cout << pt->name_<<"get a ticket, tickets = "<<--tickets <
    26. //解锁
    27. //pthread_mutex_unlock(pt->mutex_);
    28. //!!!抢完票后的操作 如果没有这个步骤,该线程会一直占用CPU执行抢票逻辑!
    29. }
    30. else
    31. {
    32. //注意这个逻辑里也要解锁!
    33. //pthread_mutex_unlock(pt->mutex_);
    34. break;
    35. }
    36. usleep(1000);
    37. }
    38. delete pt;
    39. }
    40. #define NUM 5
    41. int main()
    42. {
    43. vector<pthread_t> vp(NUM);
    44. char buffer[64];
    45. //锁初始化
    46. //1.锁类型 + 初始化函数
    47. //2.全局变量 pthread_mutex_t mutex =宏(PTHREAD_MUTEX_INITIALIZER)
    48. pthread_mutex_t mutex;
    49. pthread_mutex_init(&mutex,nullptr);
    50. for(int i=0;i
    51. {
    52. pthread_t tid;
    53. snprintf(buffer,sizeof(buffer),"thread %d ",i+1);
    54. Thread_data* pt = new Thread_data();
    55. pt->mutex_ = &mutex;
    56. pt->name_ = buffer;
    57. pthread_create(&(vp[i]),nullptr,pthread_route,pt);
    58. }
    59. for(const auto& tid : vp)
    60. {
    61. pthread_join(tid,nullptr);
    62. }
    63. return 0;
    64. }

     可以肯定的是线程资源是安全的了,但是却是一个线程在疯狂抢票,这样的行为不是错误的,但不合理!我们需要每个线程都有机会,所以引入线程同步,利用同步"信号"与等待队列来实现线程公平的竞争!


    二、线程同步

    1.1 线程同步处理抢票


    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. int tickets = 100;
    7. //初始化锁和条件变量
    8. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    9. pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    10. void *pthread_route(void *args)
    11. {
    12. string name = (char *)args;
    13. while (true)
    14. {
    15. //加锁
    16. pthread_mutex_lock(&mutex);
    17. //条件等待信号唤醒
    18. pthread_cond_wait(&cond, &mutex);
    19. if (tickets > 0)
    20. cout << name << "get a ticket, tickets = " << --tickets << endl;
    21. pthread_mutex_unlock(&mutex);
    22. }
    23. }
    24. int main()
    25. {
    26. pthread_t tid1, tid2, tid3;
    27. pthread_create(&tid1, nullptr, pthread_route, (void *)"thread 1 ");
    28. pthread_create(&tid2, nullptr, pthread_route, (void *)"thread 2 ");
    29. pthread_create(&tid3, nullptr, pthread_route, (void *)"thread 3 ");
    30. // 主线程负责每隔一秒信号唤醒条件变量
    31. while (true)
    32. {
    33. sleep(1);
    34. pthread_cond_signal(&cond);
    35. cout << "main wake up a thread ..." << endl;
    36. }
    37. pthread_join(tid1, nullptr);
    38. pthread_join(tid2, nullptr);
    39. pthread_join(tid3, nullptr);
    40. return 0;
    41. }


     1.2 如何理解"条件变量"

    首先明确一下整个代码逻辑:
    a.先加锁,保护临界资源

    b.条件等待,等待条件满足信号唤醒,否则阻塞!

    c.解锁

    当线程条件阻塞,会被放入条件队列中等待!当有信号唤醒的时候,从队列中一个一个得出!

    1.3 如何理解条件变量函数需要传锁参数

    因为等待队列被线程共享,为了保证线程入队列安全,所以需要加锁保护线程安全! 

  • 相关阅读:
    手写数字识别--神经网络实验
    205、使用消息队列实现 RPC(远程过程调用)模型的 服务器端 和 客户端
    20李沐动手学深度学习v2/参数管理
    再谈 FireBird 的自增字段用 FireDAC 来处理
    Docker安装MySQL详细步骤
    Linux基本命令总结练习(过命令关)
    被广泛使用的OAuth2.0的密码模式已经废了,放弃吧
    什么是惊群效应
    矩阵等价和向量组等价的一些问题
    Vue常用的组件库大全【前端工程师必备】
  • 原文地址:https://blog.csdn.net/qq_68926231/article/details/132583243