目录
1.为什么需要线程同步
在多线程并发访问临界资源的时候(我们已经对代码进行了加锁保护),如果有个别线程竞争锁的能力非常强,那么就会导致竞争锁能力弱的线程线程竞争不到锁,从而导致线程饥饿问题。理想的情况是多线程并发访问临界资源时,能够按照一定的顺序进行访问,也就是排队进行访问。
- 竞态条件:我们把这种因为时序问题,而导致程序异常的情况称之为竞态条件。
- 线程同步:我们把 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做线程同步。
我们以食堂打饭为例来理解:假设学校的食堂只有一个窗口,一个窗口只有一个阿姨打饭。一天,你去食堂吃饭,吃饭的人很多,人多就算了,而且还没素质(我只是假设哈),大家都不排队,想要吃饭,各凭本事。其中,就有一个人高马大 饭量也大的壮汉,把大家卡在身后,就让阿姨给他打饭,阿姨给他打完饭之后,他立马就吃完了,吃完之后立马伸手对阿姨说,“阿姨,我还要”,阿姨没办法,不把他打发走后面的同学也吃不上饭,于是阿姨又给他打了一份,这个壮汉又吃完了,又要…… 如此循环往复,后面的同学还吃不吃了?这时一个西装革履的男人出现了(食堂经理),看到了这种情况,于是心生一计,他规定 “打到饭的同学,吃完后还想吃就必须排队,要不然就永远别吃了”。壮汉一听,没办法,吃完后老老实实去排队,这是,后面的同学也能吃上饭了。
- 在这个例子中,壮汉就是竞争锁能力非常强的线程。
- 打饭的同学就是一个个的线程。
- 食堂的饭就是那把锁。
- 食堂经理就是操作系统的设计者。
为了解决这种问题,我们就需要使用条件变量。
2.条件变量相关函数
- 条件变量通常与互斥锁一起使用,以确保线程安全。
- 条件变量通常在加锁和解锁之间使用。
- 定义条件变量的类型是pthread_cond_t。
初始化
条件变量的初始化类似于互斥量的初始化,也可以分为静态初始化和动态初始化。
注意事项如下:
-
静态初始化的条件变量不需要手动销毁,因为它们是在编译时初始化的,生命周期与程序相同。
-
动态初始化的条件变量必须手动销毁,以避免资源泄漏。
静态初始化
静态初始化适用于全局或静态条件变量,通常在定义时直接初始化。使用 PTHREAD_COND_INITIALIZER
宏可以方便地初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态初始化
动态初始化适用于需要在运行时初始化条件变量的情况,或者需要指定条件变量的属性时。使用 pthread_cond_init()
函数进行初始化。
pthread_cond_init函数:
函数原型:int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr)
参数:
cond
:指向要初始化的条件变量的指针。
attr
:指向条件变量属性的指针,通常设置为NULL
,表示使用默认属性。返回值:
- 成功:返回0。
- 失败:返回错误码(非0)。
等待
条件变量通常和互斥锁配合使用,让竞争到锁的线程先去条件变量提供的队列下进行等待并释放线程竞争到的锁,等到条件就绪之后,条件变量会按照队列的先进先出规则,从自己的队列中唤醒线程,被唤醒的线程需要重新竞争到锁 才能执行后续代码。
让线程进行等待的函数是pthread_cond_wait:
函数原型:int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
参数:
cond
:指向条件变量的指针,线程将在此条件变量上等待。
mutex
:指向与条件变量关联的互斥锁的指针(在调用pthread_cond_wait
之前,线程必须已经锁定了该互斥锁)。返回值:
- 成功:返回0。
- 失败:返回错误码(非0)。
需要特别注意该函数的行为:
释放互斥锁:当线程调用
pthread_cond_wait
时,它会自动释放与条件变量关联的互斥锁(mutex
),以便其他线程可以获取该锁并修改共享数据。进入等待状态:线程进入阻塞状态,等待其他线程通过
pthread_cond_signal
函数或pthread_cond_broadcast
函数唤醒它。重新获取互斥锁:当线程被唤醒后,
pthread_cond_wait
会重新获取互斥锁(mutex
),然后函数返回。
唤醒
线程库中提供了两个函数用于唤醒在条件变量下等待的进程:
- pthread_cond_signal:唤醒一个在条件变量先等待的线程。
- pthread_cond_broadcast:唤醒在条件变量下等待的全部线程。
pthread_cond_signal函数
函数原型:int pthread_cond_signal(pthread_cond_t *cond)
参数:
cond
:指向条件变量的指针,用于唤醒在该条件变量上的等待一个线程。返回值:
- 成功:返回0。
- 失败:返回错误码(非0)。
pthread_cond_broadcast函数
函数原型:int pthread_cond_broadcast(pthread_cond_t *cond)
参数:
cond
:指向条件变量的指针,用于唤醒在该条件变量上等待的全部线程。返回值:
- 成功:返回0。
- 失败:返回错误码(非0)。
销毁
静态初始化的条件变量不需要手动销毁,而动态初始化的函数需要手动销毁,线程库中用于手动销毁条件变量的函数是:pthread_cond_destroy。
函数原型:int pthread_cond_destroy(pthread_cond_t *cond)
参数:
cond
:指向要销毁的条件变量的指针返回值:
- 成功:返回0。
- 失败:返回错误码(非0)。
3.条件变量函数使用示例
使用条件变量让线程实现按顺序访问临界资源,其实是要让一个线程来控制一批线程。我们的代码示例中,创建了一个Boss线程,创建了五个Employer线程,通过这一个Boss线程实现了对五个Employer线程的控制,让着五个Employer线程能够按照一定的顺序运行。
代码如下:
#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER; // 条件变量
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER; // 互斥锁
void *EmployerCode(void *args)
{
std::string name = static_cast<const char *>(args);
while (true)
{
// 1. 加锁
pthread_mutex_lock(&gmutex);
// 2. 一般条件变量是在加锁和解锁之间使用的
pthread_cond_wait(&gcond, &gmutex);
std::cout << "当前被叫醒的线程是: " << name << std::endl;
// 3. 解锁
pthread_mutex_unlock(&gmutex);
}
}
void *BossCode(void *args)
{
sleep(3);
std::cout << "Boss 开始工作..." << std::endl;
std::string name = static_cast<const char *>(args);
while (true)
{
pthread_cond_signal(&gcond); // 唤醒其中一个队列首部的线程
// pthread_cond_broadcast(&gcond); // 唤醒队列中所有的线程
std::cout << "Boss 唤醒一个线程..." << std::endl;
sleep(1);
}
}
void CreateBoss(std::vector<pthread_t> *tidsptr)
{
pthread_t tid;
int n = pthread_create(&tid, nullptr, BossCode, (void *)"Boss Thread");
if (n == 0)
{
std::cout << "create Boss success" << std::endl;
}
tidsptr->emplace_back(tid);
}
void CreateEmployer(std::vector<pthread_t> *tidsptr, int threadnum = 3)
{
for (int i = 0; i < threadnum; i++)
{
char *name = new char[64];
snprintf(name, 64, "Employer-%d", i + 1);
pthread_t tid;
int n = pthread_create(&tid, nullptr, EmployerCode, name);
if (n == 0)
{
std::cout << "create Employer success: " << name << std::endl;
tidsptr->emplace_back(tid);
}
}
}
void WaitThread(std::vector<pthread_t> &tids)
{
for (auto &tid : tids)
{
pthread_join(tid, nullptr);
}
}
int main()
{
std::vector<pthread_t> tids;
// 创建主线程
CreateBoss(&tids);
// 创建从线程
CreateEmployer(&tids, 5);
// 等待所有线程
WaitThread(tids);
return 0;
}
运行结果如下:
- 我们看到确实通过条件变量实现了对线程的控制(让线程排队)。
为什么pthread_cond_wait 函数中需要互斥量
- 条件等待是线程间同步的一种手段,如果只有一个线程并且条件不满足,该线程一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
- 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。