【Linux/C++多线程篇(二) 】给线程装上“红绿灯”:通俗易懂的同步互斥机制讲解 C++ 11下的多线程
⭐️在这个怀疑的年代我们依然需要信仰。个人主页YYYing.⭐️Linux/C进阶系列专栏【从零开始的linux/c进阶编程】系列上期内容【Linux/C多线程篇(一) 】多线程编程入门系列下期内容暂无目录前言当多线程遇上“交通混乱”线程的同步互斥机制一、为什么需要同步互斥二、线程互斥之互斥锁2.1、互斥锁的相关API函数接口 创建一个互斥锁 初始化互斥锁 获取锁资源 释放锁资源 销毁互斥锁2.2、互斥锁的小练习三、线程同步之无名信号量3.1、无名信号量的相关API函数接口 创建无名信号量 初始化无名信号量 申请无名信号量的资源P操作 释放无名信号量的资源V操作 销毁无名信号量3.2、互斥锁的小练习四、线程同步之条件变量4.1、条件变量的API函数接口 创建一个条件变量 初始化条件变量 消费者线程进入等待队列 生产者线程唤醒休眠队列中的任务 销毁条件变量3.2、条件变量的小练习C11中的多线程一、线程相关常用操作1.1、线程的创建1.2、线程体函数种类1.3、线程号获取1.4、线程号回收二、互斥锁的使用2.1、常用函数2.2、lock_guard的使用2.3、代码演示三、条件变量2.1、常用函数2.2、代码演示结语---⭐️封面自取⭐️---前言当多线程遇上“交通混乱”想象一下你是一个繁忙路口的交警需要同时指挥四面八方的车辆。如果没有红绿灯和交警所有车辆都凭感觉开结果必然是撞车、拥堵、混乱。在计算机世界里多个线程同时访问共享数据时也会出现类似的“交通事故”——数据错乱、程序崩溃、结果不可预测。为了让线程们有序地工作我们需要给它们装上“红绿灯”也就是同步互斥机制。本文将从生活比喻出发带你轻松理解这些看似复杂的并发工具。线程的同步互斥机制一、为什么需要同步互斥假设有一个火车票售票系统剩余票数为 1。两个线程同时执行以下操作if (tickets 0) { tickets--; cout 购票成功; }如果两个线程同时检查tickets 0发现都是 1于是都执行tickets--结果票数变成 -1但两人都以为自己买到了票。这就是典型的竞态条件——程序的结果依赖于线程执行的偶然顺序。这种问题源于线程的交错执行tickets--不是原子操作也就是指不会被线程调度机制打断的操作。它实际上分为三步读取、减一、写回。如果两个线程的步骤交错就会出错。要解决这个问题我们需要确保同一时刻只有一个线程能操作票数这就是互斥。同时可能还需要让一个线程等待另一个线程完成某件事比如等票补足再卖这就是同步。总的来说——由于同一个进程的多个线程会共享进程的资源这些被共享的资源称为临界资源多个线程对公共资源的抢占问题访问临界资源的代码段称为临界区多个线程抢占进程资源的现象称为竞态为了解决竞态我们引入了同步互斥机制下面我们就来看看系统为我们提供的几种“红绿灯”。二、线程互斥之互斥锁互斥锁的本质是一个特殊的临界资源当该临界资源被某个线程所拥有后其他线程就不能拥有该资源直到拥有该资源的线程释放掉互斥锁后其他线程才能进行抢占同一时刻一个互斥锁只 能被一个线程所拥有相当于一把获取资源的钥匙。2.1、互斥锁的相关API函数接口 创建一个互斥锁只需定义一个pthread_mutex_t 类型的变量即创建了一个互斥锁pthread_mutex_t mutex; 初始化互斥锁函数原型int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; //静态初始化头文件iostream功能初始化互斥锁变量参数说明参数1互斥锁变量的地址属于地址传递参数2互斥锁属性一般填NULL让系统自动设置互斥锁属性返回值成功返回0失败返回错误码 获取锁资源函数原型int pthread_mutex_lock(pthread_mutex_t *mutex);头文件iostream功能获取锁资源,如果要获取的互斥锁已经被其他线程锁定那么该函数会阻塞直到能够获取锁资源参数说明互斥锁地址属于地址传递返回值成功返回0失败返回错误码 释放锁资源函数原型int pthread_mutex_unlock(pthread_mutex_t *mutex);头文件iostream功能释放对互斥锁资源的拥有权参数说明互斥锁变量的地址返回值成功返回0失败返回错误码 销毁互斥锁函数原型int pthread_mutex_destroy(pthread_mutex_t *mutex);头文件iostream功能销毁互斥锁参数说明互斥锁变量的地址返回值成功返回0失败返回错误码2.2、互斥锁的小练习可以看到我们线程1和线程2都是先抢占我们锁资源然后进行释放这其中的机制依旧是时间片轮询上下文切换。#includeiostream #includecstdio #includecstring #include unistd.h using namespace std; //11、创建一个互斥锁 pthread_mutex_t mutex; //定义一个全局资源 int num 520; //定义分支线程1 void *task1(void *arg){ while(1){ sleep(1); //临界资源 //33、获取锁资源 pthread_mutex_lock(mutex); num - 10; //线程1将临界资源减少10 printf(张三取了10剩余%d\n, num); //44、释放锁资源 pthread_mutex_unlock(mutex); } } //定义分支线程2 void *task2(void *arg){ while(1){ sleep(1); //33、获取锁资源 pthread_mutex_lock(mutex); num - 20; //线程1将临界资源减少10 printf(李四取了20剩余%d\n, num); //44、释放锁资源 pthread_mutex_unlock(mutex); } } /*****************************主线程****************************/ int main() { //22、初始化互斥锁,参数NULL表示让系统自动分配互斥锁属性 pthread_mutex_init(mutex, NULL); //1、创建两个分支线程 pthread_t tid1,tid2; if(pthread_create(tid1, NULL, task1, NULL) ! 0){ printf(tid1 create error\n); return -1; } if(pthread_create(tid2, NULL, task2, NULL) ! 0){ printf(tid2 create error\n); return -1; } printf(主线程tid1 %#x, tid2 %#x\n, tid1, tid2); //2、阻塞等待线程结束 pthread_join(tid1, NULL); pthread_join(tid2, NULL); //55、释放锁资源 pthread_mutex_destroy(mutex); std::cout Hello, World! std::endl; return 0; }三、线程同步之无名信号量线程同步就是多个线程之间有先后顺序得执行这样在访问临界资源时就不会产生抢占现象了同步机制常用于生产者消费者模型消费者任务要想执行必须先执行生产者线程多个任务有顺序执行无名信号量本质上也是一个特殊的临界资源内部维护了一个value值当某个进行想要执行之前先申请该无名信号量的value资源如果value值大于0则申请资源函数接触阻塞继续执行后续操作。如果value值为0则当前申请资源函数会处于阻塞状态直到其他线程将该value值增加到大于03.1、无名信号量的相关API函数接口 创建无名信号量只需定义一个sem_t 类型的变量即可sem_t sem; 初始化无名信号量函数原型int sem_init(sem_t *sem, int pshared, unsigned int value);头文件semaphore.h功能初始化无名信号量最主要是初始化value值参数说明参数1无名信号量的地址参数2判断进程还是线程的同步0表示线程间同步非0表示进程间同步需要创建在共享内存段中参数3无名信号量的初始值返回值成功返回0失败返回-1并置位错误码 申请无名信号量的资源P操作函数原型int sem_wait(sem_t *sem);头文件semaphore.h功能阻塞申请无名信号量中的资源成功申请后会将无名信号量的value进行减1操作如果当前无名信号量的value为0则阻塞参数说明无名信号量的地址返回值成功返回0失败返回-1并置位错误码 释放无名信号量的资源V操作函数原型int sem_post(sem_t *sem);头文件semaphore.h功能将无名信号量的value值增加1操作参数说明无名信号量的地址返回值成功返回0失败返回-1并置位错误码 销毁无名信号量函数原型int sem_destroy(sem_t *sem);头文件semaphore.h功能销毁无名信号量参数说明无名信号量的地址返回值成功返回0失败返回-1并置位错误码3.2、互斥锁的小练习我们现在再来看看同步在生产者消费者模型中的应用当 进程2 想要执行之前先申请无名信号量的value资源如果value值大于0则申请资源函数解除阻塞并继续执行后续操作。如果value值为0则当前申请资源函数会处于阻塞状态直到 线程1 将该value值增加到大于0#includeiostream #includecstdio #includecstring #includeunistd.h #includesemaphore.h #includepthread.h sem_t sem; //创建生产者线程 void *task1(void *arg){ int num 5; while(num--){ sleep(1); printf(我生产了一辆特斯拉\n); //44、释放无名信号量资源 sem_post(sem); } //退出线程 pthread_exit(NULL); } //创建消费者线程 void *task2(void *arg){ int num 5; while(num--){ //33、申请无名信号量的资源 sem_wait(sem); printf(我消费了一辆特斯拉很开心\n); } //退出线程 pthread_exit(NULL); } /*******************************主程序*************************/ int main() { //22、初始化无名信号量,第一个0表示用于线程间通信第二个0表示初始值为0 sem_init(sem, 0, 0); //1、创建两个分支线程 pthread_t tid1,tid2; if(pthread_create(tid1, NULL, task1, NULL) ! 0){ printf(tid1 create error\n); return -1; } if(pthread_create(tid2, NULL, task2, NULL) ! 0){ printf(tid2 create error\n); return -1; } printf(主线程tid1 %#x, tid2 %#x\n, tid1, tid2); //2、阻塞等待线程结束 pthread_join(tid1, NULL); pthread_join(tid2, NULL); //55、销毁无名信号量 sem_destroy(sem); return 0; }四、线程同步之条件变量我们不难发现如果我们只用互斥锁那么我们的消费者和生产者只能是一对一的关系但我们要想让一个生产者对应多个消费者用互斥锁就不行了所以我们就要用到我们的条件变量了。条件变量本质上也是一个临界资源他维护了一个队列当消费者线程想要执行时先进入队列中等待生产者的唤醒。执行完生产者再由生产者唤醒在队列中的消费者这样就完成了生产者和消费者之间的同步关系。但是多个消费者在进入休眠队列的过程是互斥的所以在消费者准备进入休眠队列时我们需要使用互斥锁来进行互斥操作。4.1、条件变量的API函数接口 创建一个条件变量只需定义一个pthread_cond_t类型的全局变量即可pthread_cond_t cond; 初始化条件变量函数原型int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);pthread_cond_t cond PTHREAD_COND_INITIALIZER; //静态初始化头文件iostream功能初始化条件变量参数说明参数1条件变量的起始地址参数2条件变量的属性一般填NULL返回值成功返回0失败返回一个错误码 消费者线程进入等待队列函数原型int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);头文件iostream功能将线程放入休眠等待队列等待其他线程的唤醒参数说明参数1条件变量的地址参数2互斥锁由于多个消费者线程进入等待队列时会产生竞态为了解决竞态需要使用一个互斥锁返回值成功返回0失败返回错误码 生产者线程唤醒休眠队列中的任务函数原型int pthread_cond_broadcast(pthread_cond_t *cond);int pthread_cond_signal(pthread_cond_t *cond);头文件iostreamiostream功能唤醒条件变量维护的队列中的所有消费者线程唤醒条件变量维护的队列中的第一个进入队列的消费者线程参数说明条件变量的地址条件变量的地址返回值成功返回0失败返回错误码成功返回0失败返回错误码 销毁条件变量函数原型int pthread_cond_destroy(pthread_cond_t *cond);头文件iostream功能销毁一个条件变量参数说明条件变量的地址返回值成功返回0失败返回错误码3.2、条件变量的小练习我们再以生产者消费者模型来看看我们的用法我们既可以一个一个唤醒也可以直接一次性将消费者全部唤醒。#includeiostream #includeunistd.h #includepthread.h //11、定义一个条件变量 pthread_cond_t cond; //111、定义一个互斥锁 pthread_mutex_t mutex; //创建生产者线程 void *task1(void *arg) { /* int num 3; while(num--) { sleep(1); printf(%#x:生产了一辆特斯拉\n, pthread_self()); //44、唤醒一个消费者进行消费 pthread_cond_signal(cond); } */ sleep(3); printf(我生产了3辆特斯拉\n); //44、唤醒所有消费者线程 pthread_cond_broadcast(cond); //退出线程 pthread_exit(NULL); } //创建消费者线程 void *task2(void *arg) { //333、获取锁资源 pthread_mutex_lock(mutex); //33、进入休眠队列等待生产者的唤醒 pthread_cond_wait(cond, mutex); printf(%#x:消费了一辆特斯拉很开心\n, pthread_self()); //444、释放锁资源 pthread_mutex_unlock(mutex); //退出线程 pthread_exit(NULL); } int main() { //22、初始化条件变量 pthread_cond_init(cond, NULL); //222、初始化互斥锁 pthread_mutex_init(mutex, NULL); //1、创建两个分支线程 pthread_t tid1,tid2,tid3,tid4; if(pthread_create(tid1, NULL, task1, NULL) ! 0){ printf(tid1 create error\n); return -1; } if(pthread_create(tid2, NULL, task2, NULL) ! 0){ printf(tid2 create error\n); return -1; } if(pthread_create(tid3, NULL, task2, NULL) ! 0){ printf(tid3 create error\n); return -1; } if(pthread_create(tid4, NULL, task2, NULL) ! 0){ printf(tid4 create error\n); return -1; } printf(主线程tid1 %#x, tid2 %#x, tid3 %#x, tid4 %#x\n, tid1, tid2, tid3, tid4); //2、阻塞等待线程结束 pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_join(tid3, NULL); pthread_join(tid4, NULL); //55、销毁条件变量 pthread_cond_destroy(cond); ///555、销毁互斥锁 pthread_mutex_destroy(mutex); return 0; }C11中的多线程C11之后就支持线程支持库了也支持线程创建、互斥锁、条件变量线程支持库需要引入头文件 #includethread一、线程相关常用操作1.1、线程的创建C线程支持库本质是是面向对象的操作可以使用构造函数完成。1.2、线程体函数种类可以是任意类型的函数不必要是 void * 类型参数也是void *类型可以是全局函数也可以是类中成员函数当做线程体可以是仿函数当作线程体函数也可以是Lambda表达式当作线程体函数1.3、线程号获取this_thread::get_id()下述代码为我们展示了4种不同的线程体函数。#includeiostream #include thread using namespace std; /*****************第一个测试线程体*******************/ void ThreadFun_1(){ coutThreadFun_1 tid this_thread::get_id()endl; cout ThreadFun_1 testendl; } /*****************第二个线程体测试**********************/ void ThreadFun_2(int num, string str){ //有参无返回值函数 coutThreadFun_2 tid this_thread::get_id()endl; coutnum num str str endl; } /*******************第三个线程体测试*********************/ class ThreadClass{ public: string name; int age; void ThreadClassFun(){ //类中成员函数作为线程体函数 coutThreadFun_3 tid this_thread::get_id()endl; cout name this-name age ageendl; } }; /**********************主程序**********************/ int main(int argc, const char *argv[]){ //主程序就是主线程 //创建第一个分支线程使用无参函数完成线程体为无参无返回值函数 thread th1(ThreadFun_1); //创建第二个分支线程并向线程体中传递数据 string name zpp; thread th2(ThreadFun_2, 520, name); //此时就创建了一个分支线程线程体函数 //可以直接向线程体传递参数有多少可以传多少无需使用结构体完成 //创建第三个分支线程向线程体中传递一个类的成员函数 ThreadClass test; test.name zhangsan; test.age 18; thread th3(ThreadClass::ThreadClassFun, test); /************lambda表达式当作线程体:开发过程中用的比较多的************/ //创建第四个分支线程将lambda表达式当作线程体函数 thread th4([](int key){ coutThreadFun_4 tid this_thread::get_id()endl; coutkey keyendl; }, 999); //阻塞回收分支线程 th1.join(); th2.join(); th3.join(); th4.join(); return 0; }1.4、线程号回收阻塞方式回收线程th1.join();非阻塞方式回收线程th1.detach();此处的阻塞非阻塞回收与我们上一篇讲的并无二异说到底这些类的实现本质也是在用我们上节课c语言的那些函数所做出来的。#includeiostream #includethread using namespace std; /*****************第一个测试线程体*******************/ void ThreadFun_1(){ // 无参无返回值 for (int i 0; i 10; i){ cout ThreadFun_1 tid this_thread::get_id() endl; cout ThreadFun_1 test endl; //延时函数 this_thread::sleep_for(1s); //等待1秒时间 } } /**********************主程序**********************/ int main(int argc, const char *argv[]){ // 阻塞回收分支线程 //th1.join(); // 主程序就是主线程 // 创建第一个分支线程使用无参函数完成线程体 thread th1(ThreadFun_1); // 此时就创建了一个分支线程线程体函数为无参无返回值函数 th1.detach(); //将线程设置成分离态:主线程可以继续做自己其他事情 this_thread::sleep_for(20s); return 0; }二、互斥锁的使用互斥锁本质上是完成将多个线程使用临界资源时防止竞态我们需要在c编程中需要引入头文件 #includemutex2.1、常用函数1、构造函数创建一个互斥锁对象 2、 lock():上锁 3、 unlock():释放锁资源2.2、lock_guard的使用但在这其中我们的mutex互斥锁经常会与lock_guard进行一起使用用于在其构造时自动获取锁在析构时自动释放锁。使用std::lock_guard的好处是当std::lock_guard对象离开其作用域时会自动调用析构函数该析构函数会释放锁。这确保了在任何情况下包括由于异常等原因导致的提前退出锁都会被正确释放从而避免了忘记手动释放锁而导致的死锁问题。std::mutex myMutex; std::lock_guardstd::mutex lock(myMutex);2.3、代码演示#includeiostream #includethread #includemutex using namespace std; mutex mux; //实例化一个互斥锁 /*****************第一个测试线程体*******************/ void ThreadFun_1(){ // 无参无返回值 mux.lock(); //获取锁资源 coutendl; couttid this_thread::get_id()endl; this_thread::sleep_for(1s); cout**************************************endl; mux.unlock(); //释放锁资源 } /**********************主程序**********************/ int main(int argc, const char *argv[]){ for(int i0; i10; i){ thread th(ThreadFun_1); th.detach(); } //线程分离 this_thread::sleep_for(20s); std::cout Hello, World! std::endl; this_thread::sleep_for(20s); return 0; }三、条件变量实现一个生产者对应多个消费者问题需要引入头文件#includecondition_variable2.1、常用函数1、 构造函数创建并初始化一个条件变量 2、 wait():将消费者线程放入等待队列中 3、 唤醒线程 cv.notify_one(); 唤醒一个线程 cv.notify_all(); 唤醒所有线程2.2、代码演示不难看出与我们刚才所讲的同步机制中的条件变量逻辑是非常相似的。#includeiostream #includethread #includemutex #includecondition_variable using namespace std; //定义一个条件变量 condition_variable cv; mutex mux; //条件变量头文件 //线程支持库头文件 //互斥锁 //用于防止竞态的互斥锁 //定义生产者线程 void ThreadWrite(){ for(int i0; i5; i){ this_thread::sleep_for(2s); cout我生产了一辆特斯拉endl; cv.notify_one(); //通知一个线程可以消费了 //通知所有线程 //cv.notify_all(); } } //定义消费者线程 void ThreadRead(){ //提前先进入消费者队列 unique_lockmutex lock(mux); cv.wait(lock); cout我消费了一辆特斯拉endl; lock.unlock(); // 解锁 } int main(int argc, const char *argv[]) { //创建生产者线程 thread th1(ThreadWrite); //每隔两秒时间生产一辆特斯拉 //创建多个消费之 for(int i0; i5; i){ thread th2(ThreadRead); th2.detach(); } th1.join(); //阻塞回收线程 return 0; }结语当你掌握了“红绿灯”和“独木桥”的原理你就已经跨过了多线程编程最危险的那道门槛。接下来就是去享受多核 CPU 带来的速度激情吧我是YYYing后面还有更精彩的内容希望各位能多多关注支持一下主包。无限进步我们下次再见---⭐️封面自取⭐️---