文章目录:
- Linux线程同步
- 条件变量
- 同步概念与竟态条件
- 条件变量函数
- 为什么 pthread_cond_wait 需要互斥量?
- 条件变量使用规范
Linux线程同步
条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,队列为空,它就只能等待,直到其它线程将数据添加到队列中。在这种场景下就需要用到条件变量。
条件变量是一种线程间的同步机制,用于在一个线程等待某个条件满足时暂停执行,并在条件满足时被唤醒继续执行。
在上述示例中,可以使用条件变量来实现线程的等待和唤醒操作。当队列为空时,线程可以调用条件变量的等待函数,使线程进入等待状态,直到有其它线程向队列中添加了数据并通知条件变量,此时等待的线程就会被唤醒继续执行。
条件变量通常与互斥量一起使用,以确保线程在等待和唤醒时的安全性。在等待条件之前,线程需要先获取互斥锁,以确保在等待期间其它线程不会修改条件。而在唤醒之前,需要先释放锁,以允许其它线程获取锁并修改条件。
同步概念与竟态条件
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效的避免饥饿问题,这就叫做同步。
- 竟态条件:因为时序问题,而导致程序异常,我们称之为竟态条件。在线程场景下,这种问题也比较好理解。
在一般情况下,锁是不保证公平性的。若某个线程的优先级高,竞争资源的能力强,每次都能申请到锁,那么其它线程可能长时间竞争不到锁,导致饥饿问题。为了解决该问题,可以引入锁的公平性机制,即一个线程释放资源后,下一个获得锁的线程一定是等待时间最长的线程,这样就可以有效的避免某个线程一直占用锁而导致其它线程长时间等待。✨
示例:假设某个银行的柜台每次只能服务一个客户,而银行希望根据用户到来的先后顺序提供服务,在没有公平性的前提下,某些客户可能一直占用柜台,导致其它客户长时间等待。因此,为了确保公平性,银行引入了一个排队叫号系统,当一个客户到达银行之后,首先需要申请排队号码。当柜台空闲时,下一个序号的客户就会被叫到柜台执行任务。🎃
在该示例中,序号就是资源等待队列,柜台就是临界资源,而每个客户就是一个线程。通过引入排队叫号系统,可以让每个用户按照某种次序来获得相应的临界资源。
条件变量函数
初始化
函数 pthread_cond_init
用于初始化条件变量。它的函数定义如下:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
// cond:要初始化的条件变量
// attr:条件变量的属性,可以传入NULL,表示使用默认属性
注意:使用 PTHREAD_COND_INITIALIZER
初始化的条件变量不需要进行手动销毁。
销毁
函数 pthread_cond_destroy
函数用于销毁条件变量,释放相关资源。在不需要使用条件变量时,应该调用该函数进行清理。它的函数定义如下:
int pthread_cond_destroy(pthread_cond_t *cond)
// cond:要销毁的条件变量
等待条件满足
函数 pthread_cond_wait
用于使线程等待条件变量的信号。在调用该函数前,需要先获取互斥锁,然后在函数内部自动释放互斥锁并等待条件变量的信号。它的函数定义如下:
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// cond:需要在这个条件变量上进行等待
// mutex:与条件变量关联的互斥锁
唤醒等待
函数 pthread_cond_broadcast
用于发送广播信号,唤醒所有等待该条件变量的线程。pthread_cond_signal
函数用于发送信号,唤醒一个等待该条件变量的线程。它们的函数定义如下:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
// cond:要发送信号的条件变量
示例:代码实现了一个多线程程序,其中定义了一个条件变量 cond
和一个互斥锁 mutex
,以及一个全局退出变量 quit
。程序的功能是允许用户输入命令,如果输入的命令是 “c”,则通过调用pthread_cond_broadcast(&cond)
广播条件变量的信号,唤醒所有等待在条件变量上的线程。如果输入的命令是 “q” ,则设置全局退出变量 quit
为 true
,并跳出循环,使程序退出。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 定义一个条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 定义一个互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 当前用不上,但是接口需要
// 定义一个全局退出变量
volatile bool quit = false;
void *waitCommand(void *args)
{
pthread_detach(pthread_self());
while (!quit)
{
// 执行了下面的代码,证明某一种条件不就绪,需要该线程进行等待
// 三个线程,都会在条件变量下进行排队等待
pthread_cond_wait(&cond, &mutex); // 让对应的线程进行等待,等待被唤醒
cout << "thread id : " << pthread_self() << " run..." << endl;
}
}
int main()
{
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, waitCommand, nullptr);
pthread_create(&t2, nullptr, waitCommand, nullptr);
pthread_create(&t3, nullptr, waitCommand, nullptr);
while (true)
{
char n;
cout << "请输入你的command(c/q): ";
cin >> n;
if (n == 'c')
pthread_cond_broadcast(&cond);
else
{
quit = true;
break;
}
usleep(1000);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
return 0;
}
运行程序,结果如下:
当输入 “c” 的时候,所有在该条件变量下等待的线程被唤醒执行任务;当输入 “q” 时,线程退出。
为什么 pthread_cond_wait 需要互斥量?
- 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所有必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
- 条件变量不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
- 线程在进入临界区之前,需要先获取互斥锁。然后判断条件是否满足,如果条件不满足,则调用
pthread_cond_wait
函数进入等待状态,在进入等待状态之前,pthread_cond_wait
函数会自动释放互斥锁,允许其它线程获取互斥锁并修改共享数据。 - 当其它线程修改了共享数据,并调用
pthread_cond_signal
或pthread_cond_broadcast
函数发送条件变量的信号时,在条件变量上等待的线程会被唤醒。 - 被唤醒的线程会重新获取互斥锁,并检查条件是否满足。如果条件满足,线程会继续执行后续的操作;如果条件不满足,线程也会继续等待。
- 最后,线程完成了对共享数据的操作后,会释放互斥锁,让其它线程也可以继续访问该共享数据。
一个错误的设计
按照上面的说法,当进入临界区时,先上锁,发现条件不满足,就进行解锁,然后在该条件变量下进行等待。如下代码:
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
- 由于解锁和等待不是原子操作。调用解锁之后,
pthread_cond_wait
之前,如果已经有其它线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_cond_wait
将错过这个信号,可能会导致线程永远阻塞在这个pthread_cond_wait
。所以解锁和等待必须是一个原子操作。 int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex);
进入该函数后,会去看条件是否等于0?若等于0,则说明不满足条件,此时会先将对应的互斥锁解锁,直到pthread_cond_wait
函数返回时再将条件变量该为1,并给对应的互斥量加锁。
条件变量使用规范
等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);