C++ std::condition_variable

std::condition_variable 是C++11引入的一个同步机制,主要用于在线程之间进行事件通知协调。它是标准库中的一个类,声明在 <condition_variable> 头文件中。std::condition_variable 允许一个或多个线程等待另一个线程发送的信号(通过条件变量来实现的),从而基于某些条件进行协调动作。

wait函数

//版本一
void wait(unique_lock<mutex>& lck);
//版本二
template<class Predicate>
void wait(unique_lock<mutex>& lck, Predicate pred);

函数说明:

  • 调用第一个版本的wait函数时只需要传入一个互斥锁,线程调用wait后会立即被阻塞,直到被唤醒。
  • 调用第二个版本的wait函数时除了需要传入一个互斥锁,还需要传入一个返回值类型为bool的可调用对象,与第一个版本的wait不同的是,当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么该线程还需要继续被阻塞。

为什么调用wait系列函数时需要传入一个互斥锁?

  • 因为wait系列函数一般是在临界区中调用的,为了让当前线程调用wait阻塞时其他线程能够获取到锁,因此调用wait系列函数时需要传入一个互斥锁,当线程被阻塞时这个互斥锁会被自动解锁,而当这个线程被唤醒时,又会自动获得这个互斥锁。
  • 因此wait系列函数实际上有两个功能,一个是让线程在条件不满足时进行阻塞等待,另一个是让线程将对应的互斥锁进行解锁。

 为什么调用wait系列函数时,传入互斥锁的类型必须是unique_lock?

  • 条件变量在等待时需要释放互斥锁,并在条件满足时重新获取锁。std::unique_lock 提供了这种能力。它可以在内部处理锁的解锁和重新加锁操作,这是一个非常关键的功能。在调用条件变量的 wait 方法时,std::unique_lock 会在等待前解锁互斥锁,并在等待结束(无论是条件满足还是虚假唤醒)后重新锁定互斥锁。
  • 条件变量在等待时需要释放互斥锁,并在条件满足时重新获取锁。std::unique_lock 提供了这种能力。它可以在内部处理锁的解锁和重新加锁操作,这是一个非常关键的功能。在调用条件变量的 wait 方法时,std::unique_lock 会在等待前解锁互斥锁,并在等待结束(无论是条件满足还是虚假唤醒)后重新锁定互斥锁。

notify系列成员函数

notify系列成员函数的作用就是唤醒等待的线程,包括notify_one和notify_all。

  • notify_one:唤醒等待队列中的首个线程,如果等待队列为空则什么也不做。
  • notify_all:唤醒等待队列中的所有线程,如果等待队列为空则什么也不做。

注意: 条件变量下可能会有多个线程在进行阻塞等待,这些线程会被放到一个等待队列中进行排队。

虚假唤醒(Spurious Wakeup)

虚假唤醒(Spurious Wakeup) 是指线程没有明确的通知却从等待状态被唤醒的情况。这种现象虽然在理论上是可能的,但在某些平台上也会真实发生。因此,等待条件的线程必须在唤醒后重新检查相关条件,而不仅仅依赖于通知。

为了应对虚假唤醒问题,通常的做法是在等待通知时使用 while 循环来检查条件

实现两个、三个线程交替打印

该题目主要考察的就是线程的同步和互斥。

  • 互斥:两个线程都在向控制台打印数据,为了保证两个线程的打印数据不会相互影响,因此需要对线程的打印过程进行加锁保护。
  • 同步:两个线程必须交替进行打印,因此需要用到条件变量让两个线程进行同步,当一个线程打印完再唤醒另一个线程进行打印。

但如果只有同步和互斥是无法满足题目要求的。

  • 首先,我们无法保证哪一个线程会先进行打印,不能说先创建的线程就一定先打印,后创建的线程先打印也是有可能的。
  • 此外,有可能会出现某个线程连续多次打印的情况,比如线程1先创建并打印了一个数字,当线程1准备打印第二个数字的时候线程2可能还没有创建出来,或是线程2还没有在互斥锁上进行等待,这时线程1就会再次获取到锁进行打印。

因此,这里还需要定义一个flag变量,该变量的初始值设置为true。

  • 假设让线程1打印奇数,线程2打印偶数。那么就让线程1调用wait函数阻塞等待时,传入的可调用对象返回flag的值,而让线程2调用wait函数阻塞等待时,传入的可调用对象返回!flag的值。
  • 由于flag的初始值是true,就算线程2先获取到互斥锁也不能进行打印,因为最开始线程2调用wait函数时,会因为可调用对象的返回值为false而被阻塞,这就保证了线程1一定先进行打印。
  • 为了让两个线程交替进行打印,因此两个线程每次打印后都需要更改flag的值,线程1打印完后将flag的值改为false并唤醒线程2,这时线程2被唤醒时其可调用对象的返回值就变成了true,这时线程2就可以进行打印了。
  • 当线程2打印完后再将flag的值改为true并唤醒线程1,这时线程1就又可以打印了,就算线程2想要连续打印也不行,因为如果线程1不打印,那么线程2的可调用对象的返回值就一直为false,对于线程1也是一样的道理。

两个线程交替打印数字

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool turnA = true; // 轮到A线程打印

void printA() {
    for (int i = 1; i <= 10; i += 2) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [](){ return turnA; });
        std::cout << "Thread A: " << i << std::endl;
        turnA = false;
        cv.notify_all();
    }
}

void printB() {
    for (int i = 2; i <= 10; i += 2) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [](){ return !turnA; });
        std::cout << "Thread B: " << i << std::endl;
        turnA = true;
        cv.notify_all();
    }
}

int main() {
    std::thread t1(printA);
    std::thread t2(printB);
    t1.join();
    t2.join();
    return 0;
}

 三个线程交替打印数字

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
int turn = 0; // 用于标识哪个线程应当打印

void printNum(int threadId) {
    for (int i = threadId; i <= 30; i += 3) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [threadId]{ return turn == threadId; });
        std::cout << "Thread " << threadId << ": " << i << std::endl;
        turn = (turn + 1) % 3;
        cv.notify_all();
    }
}

int main() {
    std::thread t1(printNum, 0);
    std::thread t2(printNum, 1);
    std::thread t3(printNum, 2);
    t1.join();
    t2.join();
    t3.join();
    return 0;
}

 

 

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

**K

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值