目录
条件变量
头文件
主要操作函数
1、等待操作
2、唤醒操作
使用示例
信号量
头文件
主要操作函数
1、信号量初始化
2、等待操作(P操作)
3、信号操作(V操作)
4、获取信号量值
5、销毁信号量
使用示例
互斥锁
头文件
使用示例
当我们需要给多个线程的指定执行顺序的时候,我们通常有多种方法:
- 条件变量
- 信号量
- 互斥锁
在这篇文章里,会介绍如何使用这三种方式来为多个线程指定执行顺序,以及在使用的时候需要主义的地方。
条件变量
条件变量是C++11引入的同步原语,用于在多线程环境中实现线程间的等待和通知机制。它允许一个或多个线程等待某个条件成立,当条件满足时,其他线程可以通知等待的线程继续执行,一般需要配合unique_lock使用。
头文件
#include <condition_variable>
主要操作函数
1、等待操作
a)基本形式
void wait(std::unique_lock<std::mutex>& lock);
b)带谓词形式
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);
两者的区别在于处理虚假唤醒的情况比较明显,这个在后面介绍哈。
2、唤醒操作
a)唤醒单个线程
void notify_one() noexcept;
特点:唤醒等待队列中的一个线程,具体是哪个线程是未定义的
b)唤醒所有线程
void notify_all() noexcept;
特点:唤醒等待队列中的所有线程,性能开销比较大,但是确保所有等待线程都被唤醒。
这里需要介绍一下虚假唤醒的问题
虚假唤醒是指线程在没有收到notify_one或者notify_all调用的情况,从wait状态中被唤醒。为什么会出现虚假唤醒的情况呢?因为可能会出现系统信号中断条件变量的等待(SIGINT),或者因为底层I/O操作等底层系统调用中断,导致pthread_cond_wait() 被中断返回,因此出现虚假唤醒的情况。
在上面,我们介绍了两种等待的方式,他们在处理虚假唤醒的情况表现有所不同。
带谓词的等待方式,会自动处理虚假唤醒,不需要我们再进行手动处理,那么他是怎么做到自动处理的呢,他的内部实现等价如下代码,就是在循环中不断判断条件是否满足,以此来处理虚假唤醒的情况。
// 带谓词的wait()函数的内部实现等价于:
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred) {while (!pred()) { // 关键:自动循环检查wait(lock); // 调用基本的wait()}// 退出循环时,保证 pred() 返回 true
}
基本的等待方式 需要我们手动处理虚假唤醒的情况,如下代码是有问题的:
void wrong_basic_wait() {std::unique_lock<std::mutex> lock(mtx);// 错误:只等待一次,不处理虚假唤醒cv.wait(lock);// 假设条件一定满足 - 危险!if (data_ready) {process_data();}
}
如果因为底层系统调用中断了等待,但是此时条件并不满足,比如数据并未准备好,会出现未定义的情况,因此,我们需要模仿带谓词的等待方式的等价写法,在循环中判断,如下:
void correct_basic_wait() {std::unique_lock<std::mutex> lock(mtx);// 正确:使用循环处理虚假唤醒while (!condition_satisfied()) {cv.wait(lock);// 如果是虚假唤醒,循环会继续等待// 如果条件真的满足,循环会退出}// 这里保证条件一定满足process_data();
}
使用示例
1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/description/
class Foo {condition_variable m_cv;mutex m_mtx;int m_nFlg;
public:Foo() {m_nFlg=1;}void first(function<void()> printFirst) {// printFirst() outputs "first". Do not change or remove this line.unique_lock<mutex> lock(m_mtx);m_cv.wait(lock,[=](){return m_nFlg==1;});printFirst();m_nFlg=2;m_cv.notify_all();}void second(function<void()> printSecond) {// printSecond() outputs "second". Do not change or remove this line.unique_lock<mutex> lock(m_mtx);m_cv.wait(lock,[=](){return m_nFlg==2;});printSecond();m_nFlg=3;m_cv.notify_all();}void third(function<void()> printThird) {// printThird() outputs "third". Do not change or remove this line.unique_lock<mutex> lock(m_mtx);m_cv.wait(lock,[=](){return m_nFlg==3;});printThird();m_nFlg=1;m_cv.notify_all();}
};
信号量
信号量的本质就是一个非负整数计数器,支持两个原子操作:P(等待/减少)、V(信号/增加)
头文件
#include <semaphore.h>
主要操作函数
1、信号量初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数说明:
- sem:指向信号量(sem_t)的指针
- pshared:0表示线程间共享,非0表示进程间共享
- value:信号量的初始值
返回值
- 返回0:初始化成功
- 返回-1:初始化失败,同时设置errno的错误码。
2、等待操作(P操作)
信号量等待有三种方式
a)sem_wait()-阻塞等待
int sem_wait(sem_t *sem);
特点:如果信号量值为0,线程会一直阻塞等待,知道信号量可用。
b)sem_trywait()-非阻塞等待
int sem_trywait(sem_t *sem);
特点:非阻塞等待,立即返回,不会等待,如果信号量不可用,立即返回-1,不会造成线程阻塞的情况,适用于轮询场景
c)sem_timedwait() - 超时等待
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
特点:在指定时间内等待,超时后返回-1,使用绝对时间戳,不是相对时间。
这个参数比较多,这里演示下用法:
struct timespec结构体用于存储超时时间:
- tv_sec:秒数
- tv_nsec:纳秒数
#include <semaphore.h>
#include <iostream>
#include <time.h>
#include <errno.h>void timed_work() {struct timespec timeout;clock_gettime(CLOCK_REALTIME, &timeout);timeout.tv_sec += 5; // 5秒后超时int result = sem_timedwait(&sem, &timeout);if (result == 0) {std::cout << "在超时前获取到信号量" << std::endl;// 执行临界区代码sem_post(&sem);} else {if (errno == ETIMEDOUT) {std::cout << "等待超时,放弃获取" << std::endl;}}
}
下面的代码作用是获取当前的系统时间,CLOCK_REALTIME表示使用系统实时时钟
clock_gettime(CLOCK_REALTIME, &timeout);
3、信号操作(V操作)
释放信号量,也就是将信号量的值+1。
int sem_post(sem_t *sem);
4、获取信号量值
int sem_getvalue(sem_t *sem, int *sval);
5、销毁信号量
这种只能用于未命名的信号量,比如我们直接定义的sem_t sem,就属于未命名信号量
int sem_destroy(sem_t *sem);
使用示例
1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/description/这题希望我们指定三个线程的执行顺序,我们可以定义三个信号量来进行控制
class Foo {sem_t s1,s2,s3;
public:Foo() {sem_init(&s1,0,1);sem_init(&s2,0,0);sem_init(&s3,0,0);}~Foo() {sem_destroy(&s1);sem_destroy(&s2);sem_destroy(&s3);}void first(function<void()> printFirst) {// printFirst() outputs "first". Do not change or remove this line.sem_wait(&s1);printFirst();sem_post(&s2);}void second(function<void()> printSecond) {// printSecond() outputs "second". Do not change or remove this line.sem_wait(&s2);printSecond();sem_post(&s3);}void third(function<void()> printThird) {// printThird() outputs "third". Do not change or remove this line.sem_wait(&s3);printThird();sem_post(&s1);}
};
互斥锁
头文件
#include <mutex>
使用示例
因为互斥锁比较简单这里,直接展示使用示例:1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/
class Foo {mutex mtx1,mtx2,mtx3;
public:Foo() {mtx2.lock();mtx3.lock();}void first(function<void()> printFirst) {// printFirst() outputs "first". Do not change or remove this line.mtx1.lock();printFirst();mtx2.unlock();}void second(function<void()> printSecond) {// printSecond() outputs "second". Do not change or remove this line.mtx2.lock();printSecond();mtx3.unlock();}void third(function<void()> printThird) {// printThird() outputs "third". Do not change or remove this line.mtx3.lock();printThird();mtx1.unlock();}
};