目录
引入
饥饿问题 -- 线程同步
介绍
比如说在之前的代码中,可能会出现某个线程可以频繁的抢占到锁资源:
- 因为锁资源是它释放的,如果他不用执行其他代码,自然也就比别人更快地重新循环,继续申请到锁
- 但是这并不合理,因为还有其他线程在等待着锁
- 如果它一个线程就完成了所有任务,那其他线程存在的意义就没有了,这就导致其他线程出现饥饿问题
解决
- 所以,我们需要对等待资源的线程进行管理,比如,,利用排队的原理
- 让上一个抢到资源的线程排到队尾,这样就可以避免同一个线程频繁拿到资源的问题
- 并且,谁先来等待这个资源,谁就排前面,而不是让线程之间自行竞争
- 这样,访问资源的线程就具有了顺序性
- 而按照特定顺序访问临界资源的过程,就叫做线程同步
等待资源就绪 -- 条件变量
介绍
在访问临界资源前,我们需要等待临界资源就绪,才会进行处理:
while (true) { int n = pthread_mutex_lock(info->pmux); assert(n == 0); if (count > 0) //判断成功才会执行任务 { usleep(1000); // 模拟可能花费的时间 cout << info->name << " : " << count << endl; --count; n = pthread_mutex_unlock(info->pmux); assert(n == 0); } else { n = pthread_mutex_unlock(info->pmux); assert(n == 0); break; } usleep(1000); // 模拟抢票成功后的花费时间(为了尽量让不同线程去抢票) }
- 这里我们就需要判断count,只有>0时,才会进行抢票
- 判断也是一种访问,而访问临界资源需要在锁范围内,所以每次判断都需要申请锁
- 如果count>0还好,申请锁后确实对临界资源进行了操作
- 但count为0,线程还要申请锁,释放锁,就很没必要
- 并且我们并不确定此时count究竟是多少,有没有>0,线程就需要一直进行申请锁,判断,释放锁的过程,非常浪费资源和时间
解决
- 我们可以参考现实中的处理方式,比如:
- 当你需要某个东西,但它此时并没有,而且不确定什么时候会有
- 难道你会每天都去店里问吗?不会的,我们一般都是留个电话,让工作人员在有的时候通知我们就行
- 所以,类比到这里的情况,线程不需要陷入循环,来不断申请锁去判断资源是否就绪,而是等待通知
- 当资源就绪时,某个东西来通知等待中的进程即可
- 而实现这一过程,需要用到条件变量
概念
条件变量
用于在线程之间建立一种通信机制,允许线程在某个条件不满足时等待,当条件满足时再继续执行
线程同步
确保多个线程之间的操作按照某种有序的方式执行,以避免竞态条件和确保程序的正确性
竞态条件
- 在多线程环境下,程序的执行结果依赖于调度器调取线程的顺序,而这种结果是不确定的
- 当多个线程同时访问和修改共享资源时,如果没有适当的同步机制,就可能导致竞态条件(也就是因为时序问题导致的程序异常)
- 竞态条件可能引发各种问题,包括数据不一致、死锁等
条件变量接口
返回值
这里的接口都在pthread库中,而库中函数如果返回值是int,一般都是:
- 成功,返回0
- 失败,返回错误码
初始化
pthread_cond_init()
和互斥量的初始化是一样的
函数原型
cond
传入条件变量类型的指针作为输出型参数
attr
条件变量的属性
一般设置为nullptr即可,表示使用默认属性
pthread_cond_destroy()
同样也需要搭配destroy函数来手动销毁
PTHREAD_COND_INITIALIZER
- 使用宏来初始化一个全局条件变量
- 不需要手动销毁
等待条件满足
pthread_cond_wait()
函数原型
这里有两个函数,都可以用来让线程等待某个条件变量
- 其中,timewait可以设置超时时间,如果在规定的时间内没有收到通知,则函数返回,这里我们不使用它
- 我们主要来介绍wait
cond
指定线程等待某个条件变量
mutex
这里是需要传入一个互斥量
互斥量哪里来呢?
- 想想我们为什么需要用到条件变量
- 是为了让线程在临界资源未就绪的情况下等待通知,而不是频繁去确认
- 所以,条件变量的使用一定是在锁范围内的
- 自然也就有对应的锁变量可以作为参数传入进去
为什么要传入一个锁呢?
- 在后面的代码中我们再来讨论
唤醒线程
都是传入一个条件变量的地址作为参数,但功能的细节不同
pthread_cond_broadcast
函数原型
给所有等待cond条件变量的线程,都发送资源就绪的通知(也就是将它们唤醒)
pthread_cond_signal
函数原型
通知等待队列中的一个线程
而具体通知哪一个线程,依赖于底层的调度策略
示例代码
让多个线程执行不同任务
代码
#include <iostream> #include <pthread.h> #include <string> #include <vector> using namespace std; #define TH_NUM 3 typedef void *(*func_t)(void *); void *func1(void *args) { string name = *((string *)args); cout << "im " << name << " , " << "work 1 ... " << endl; return nullptr; } void *func2(void *args) { string name = *((string *)args); cout << "im " << name << " , " << "work 2 ... " << endl; return nullptr; } void *func3(void *args) { string name = *((string *)args); cout << "im " << name << " , " << "work 3 ... " << endl; return nullptr; } void test1() { func_t funcs[TH_NUM] = {func1, func2, func3}; // 存放线程任务 vector<pthread_t> tids; // 存放所有线程的tid for (int i = 0; i < TH_NUM; ++i) { string name = "pthread"; name += to_string(i + 1); pthread_t tid; pthread_create(&tid, nullptr, funcs[i], (void *)&name); tids.push_back(tid); } for (auto tid : tids) { pthread_join(tid, nullptr); } cout << "join success" << endl; }
介绍
这里我们采用数组来存放函数指针,然后派发给不同线程
封装统一入口函数
代码
#define TH_NUM 3 typedef void *(*func_t)(const string& name); struct Data { Data(const string &name, func_t func) : _name(name), _func(func) {} string _name; func_t _func; }; void *func1(const string& name) { cout << "im " << name << " , " << "work 1 ... " << endl; return nullptr; } void *func2(const string& name) { cout << "im " << name << " , " << "work 2 ... " << endl; return nullptr; } void *func3(const string& name) { cout << "im " << name << " , " << "work 3 ... " << endl; return nullptr; } void *entry(void *args) { Data *pd = (Data *)args; pd->_func(pd->_name); // 将data中的成员作为另一成员的参数 } void test1() { func_t funcs[TH_NUM] = {func1, func2, func3}; // 存放线程任务 vector<pthread_t> tids; // 存放所有线程的tid for (int i = 0; i < TH_NUM; ++i) { string arr = "pthread"; arr += to_string(i + 1); Data *pdata = new Data(arr, funcs[i]); // 在这里为线程分配任务 pthread_t tid; pthread_create(&tid, nullptr, entry, (void *)pdata); // 所有线程都可以使用统一的入口函数entry,但执行的是不同的任务 tids.push_back(tid); } for (auto tid : tids) { pthread_join(tid, nullptr); } cout << "join success" << endl; }
介绍
这里我们用entry作为每个线程任务函数的入口
- 采取在内部调用某个函数,让线程去执行的方式,实现不同线程不同任务
- 那么这个函数就可以不受类型限制了,也就不用强转了
- 并且,我们将线程要用到的变量+任务函数,全部封装在一个类中,然后传入entry即可
熟悉了一下多线程的使用,接下来就开始正题,来使用我们介绍的条件变量 :
唤醒等待同一条件变量的线程(两种方式)
引入
我们多线程的调度顺序由调度器决定,但如果我们想要自行控制哪个线程执行该怎么办呢?
这里就可以使用我们的条件变量了:
- 先让所有线程都陷入等待状态
- 之后,由主线程依次唤醒线程,而这个唤醒顺序就是陷入等待的顺序
- 我们可以通过控制陷入等待的顺序,来控制线程执行的顺序
代码
唤醒全部线程
#include <iostream> #include <pthread.h> #include <string> #include <vector> #include <cstdlib> #include <unistd.h> using namespace std; #define TH_NUM 3 typedef void *(*func_t)(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond); struct Data { Data(const string &name, func_t func, pthread_mutex_t *pmutex, pthread_cond_t *pcond) : _name(name), _func(func), _pcond(pcond), _pmutex(pmutex) {} string _name; func_t _func; pthread_mutex_t *_pmutex; pthread_cond_t *_pcond; }; void *func1(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond) { while (true) { pthread_cond_wait(pcond, pmutex); cout << "im " << name << " , " << "work 1 ... " << endl; } return nullptr; } void *func2(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond) { while (true) { usleep(10); pthread_cond_wait(pcond, pmutex); cout << "im " << name << " , " << "work 2 ... " << endl; } return nullptr; } void *func3(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond) { while (true) { usleep(20); pthread_cond_wait(pcond, pmutex); cout << "im " << name << " , " << "work 3 ... " << endl; } return nullptr; } void *entry(void *args) { Data *pd = (Data *)args; pd->_func(pd->_name, pd->_pmutex, pd->_pcond); // 将data中的成员作为另一成员的参数 } void test1() { func_t funcs[TH_NUM] = {func1, func2, func3}; // 存放线程任务 vector<pthread_t> tids; // 存放所有线程的tid // 初始化锁和条件变量 pthread_cond_t cond; pthread_cond_init(&cond, nullptr); pthread_mutex_t mutex; pthread_mutex_init(&mutex, nullptr); for (int i = 0; i < TH_NUM; ++i) { string arr = "pthread"; arr += to_string(i + 1); Data *pdata = new Data(arr, funcs[i], &mutex, &cond); // 在这里为线程分配任务 pthread_t tid; pthread_create(&tid, nullptr, entry, (void *)pdata); // 所有线程都可以使用统一的入口函数entry,但执行的是不同的任务 tids.push_back(tid); } // 最开始,线程首先都按照顺序处于等待状态 sleep(3); // 3s后,唤醒所有线程 int count = 3; while (true) { pthread_cond_broadcast(&cond); sleep(1); } for (auto tid : tids) { pthread_join(tid, nullptr); } cout << "join success" << endl; pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); }
唤醒单个线程
while (true) { pthread_cond_signal(&cond); sleep(1); }
介绍
- 这里我们用[设置线程睡眠时间长短]的方式,手动设置线程陷入等待的顺序 -> 线程被唤醒的顺序 -> 线程执行的顺序
- 主线程在3s后,每隔1s唤醒所有线程:
- 每隔1s唤醒单个线程:
结果虽然看起来相同,但输出的效果并不同
- 使用broadcast时,每隔1s三个线程都在执行
- 使用signal时,每隔1s只执行一个线程
- 但都是按照我们设定的123顺序执行的
临界资源达成某种条件,退出线程(卡住,无法退出)
代码
#include <iostream> #include <pthread.h> #include <string> #include <vector> #include <cstdlib> #include <unistd.h> using namespace std; #define TH_NUM 3 typedef void *(*func_t)(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond); volatile bool quit = false; // 让编译器不做优化,每次访问都需要从内存中拿取数据 struct Data { Data(const string &name, func_t func, pthread_mutex_t *pmutex, pthread_cond_t *pcond) : _name(name), _func(func), _pcond(pcond), _pmutex(pmutex) {} string _name; func_t _func; pthread_mutex_t *_pmutex; pthread_cond_t *_pcond; }; void *func1(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond) { while (!quit) { pthread_cond_wait(pcond, pmutex); cout << "im " << name << " , " << "work 1 ... " << endl; } cout << name << " quit" << endl; return nullptr; } void *func2(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond) { while (!quit) { pthread_cond_wait(pcond, pmutex); cout << "im " << name << " , " << "work 2 ... " << endl; } cout << name << " quit" << endl; return nullptr; } void *func3(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond) { while (!quit) { pthread_cond_wait(pcond, pmutex); cout << "im " << name << " , " << "work 3 ... " << endl; } cout << name << " quit" << endl; return nullptr; } void *entry(void *args) { Data *pd = (Data *)args; pd->_func(pd->_name, pd->_pmutex, pd->_pcond); // 将data中的成员作为另一成员的参数 } void test1() { func_t funcs[TH_NUM] = {func1, func2, func3}; // 存放线程任务 vector<pthread_t> tids; // 存放所有线程的tid // 初始化锁和条件变量 pthread_cond_t cond; pthread_cond_init(&cond, nullptr); pthread_mutex_t mutex; pthread_mutex_init(&mutex, nullptr); for (int i = 0; i < TH_NUM; ++i) { string arr = "pthread"; arr += to_string(i + 1); Data *pdata = new Data(arr, funcs[i], &mutex, &cond); // 在这里为线程分配任务 pthread_t tid; pthread_create(&tid, nullptr, entry, (void *)pdata); // 所有线程都可以使用统一的入口函数entry,但执行的是不同的任务 tids.push_back(tid); } // 最开始,线程首先都按照顺序处于等待状态 sleep(3); // 3s后,唤醒所有线程 int count = 3; while (true) { --count; if (!count) { quit = true; // 让所有线程退出 cout << "quit true" << endl; pthread_cond_broadcast(&cond); //修改quit后,需要让线程醒来才能获取到quit break; } pthread_cond_broadcast(&cond); sleep(1); } for (auto tid : tids) { pthread_join(tid, nullptr); } cout << "join success" << endl; pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); }
介绍
- 我们在count减为0时,将quit这个全局变量改为true,这样线程在进行循环条件的判断时,就可以退出循环了
- 但是,运行结果却只有我们的线程1退出了:
- 为什么呢?
- 原因就在于,我们并没有加入锁
- (还记得pthread_cond_wait中传入的锁变量吗,我们并没有用到它)
加入锁
代码
void *func1(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond) { while (!quit) { pthread_mutex_lock(pmutex); pthread_cond_wait(pcond, pmutex); cout << "im " << name << " , " << "work 1 ... " << endl; pthread_mutex_unlock(pmutex); } cout << name << " quit" << endl; return nullptr; } void *func2(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond) { while (!quit) { pthread_mutex_lock(pmutex); pthread_cond_wait(pcond, pmutex); cout << "im " << name << " , " << "work 2 ... " << endl; pthread_mutex_unlock(pmutex); } cout << name << " quit" << endl; return nullptr; } void *func3(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond) { while (!quit) { pthread_mutex_lock(pmutex); pthread_cond_wait(pcond, pmutex); cout << "im " << name << " , " << "work 3 ... " << endl; pthread_mutex_unlock(pmutex); } cout << name << " quit" << endl; return nullptr; }
介绍
加上锁之后,确实是退出成功了:
但是,究竟原理是什么呢?
- 先回想一下,我们引入条件变量,是为了解决 -- 当临界资源没有就绪的时候,线程依然会频繁访问临界资源来确认其是否就绪 的问题
- 因为它不确定到底什么时候会就绪
- 但是这样的行为是不合理的
- 所以,我们引入条件变量 -- 当资源未就绪时,线程是等待别人告诉它已经就绪的消息,而不是傻不愣登的自己去一直问
- 所以,这个函数的应用场景就应该是在资源不就绪时,所以必然在锁的范围内
- 也就必然会对传入的锁变量有一些处理
- 但如果我们不加锁,它可能就会出现一些问题 -- 比如,线程无法按照我们预想正常退出的问题