C++并发编程(7):条件变量(conditional variable)、wait( )与notify_one( )、spurious wakeups(虚假唤醒)

文章介绍了线程间的同步操作,强调了条件变量在C++中的重要性。条件变量允许线程等待特定事件发生,避免了无效的资源浪费和过度休眠问题。通过示例代码展示了如何使用`std::condition_variable`进行等待和通知操作,以及如何处理虚假唤醒。文章还提到了`std::future`作为另一种同步工具。
摘要由CSDN通过智能技术生成

并发操作的同步

前面学习了如何保护线程间的共享数据。然而,有时候我们不仅需要保护共享数据,还需要令独立线程上的行为同步。例如,某线程只有先等另一线程的任务完成,才可以执行自己的任务。一般而言,线程常常需要等待特定事件的发生,或等待某个条件成立。只要设置一个“任务完成”的标志,或者利用共享数据存储一个类似的标志,通过定期查验该标志就可以满足需求,但这远非理想方法

上述线程间的同步操作很常见,C++标准库专门为之提供了处理工具:条件变量(conditional variable)和future

条件变量(conditional variable)

设想你坐夜行列车外出。如果要保证在正确的站点下车,一种方法是彻夜不眠,留心列车停靠的站点,那样就不会错过。可是,到达时你可能会精神疲倦。或者,你可以查看时刻表,按预定到达时间提前设定闹钟,随后安心入睡。这种方法还算管用,一般来说,你不会误站。但若列车晚点,你反而会太早醒来;也可能不巧,闹钟的电池刚好耗尽,结果你睡过头而错过下车站点。最理想的方法是,安排人员或设备,无论列车在什么时刻抵达目的站点,都可以将你唤起,那么你大可“高枕无忧”

同样的,如果线程甲要等待线程乙完成任务,可以采取集中不同的方式

方式一:在共享数据内部维护一标志(受互斥保护),线程乙完成任务后,就设置标志成立

该方式存在双重浪费:线程甲须不断查验标志,浪费原本有用的处理时间;另外,一旦互斥被锁住,则其他任何线程无法再加锁。这两点都是线程甲的弊病:如果它正在运行,就会限制线程乙可用的算力;还有,线程甲每次查验标志,都要锁住互斥以施加保护,那么,若线程乙恰好同时完成任务,也意欲设置标志成立,则无法对互斥加锁。这就像是你整晚熬夜,不停地与列车司机攀谈,于是他不得不放慢车速,因为你老使他走神,结果列车晚点。类似地,线程甲白白耗费了计算资源,它们本来可用于系统中的其它线程,最终导致毫无必要的等待时间

方式二:让线程甲调用 std:this_thread:islep for( )函数,在各次查验之间短期休眠

bool flag;
mutex m;
void wait_for_flag()
{
	unique_lock<mutex> lk(m);
	while(!flag)
	{
		lk.unlock();
		this_thread::sleep_for(chrono::milliseconds(100));
		lk.lock();
	}
}

上面的代码在每轮循环中,先将互斥解锁,随之休眠,再重新加锁,从而其它线程有机会获取锁,得以设置标志成立

这确有改进,因为线程休眠,所以处理时间不再被浪费。然而,休眠期的长短却难以预知。休眠期太短,线程仍会频繁查验,虚耗处理时间;休眠期太长,则令线程过度休眠。如果线程乙完成了任务,线程甲却没有被及时唤醒,就会导致延迟。过度休眠很少直接影响普通程序的运作。但是,对于高速视频游戏,过度休眠可能会造成丢帧;对于实时应用,可能会使某些时间片计算超时

方式三:使用C++标准库的工具等待事件发生

以上述甲、乙两线程的二级流水线模式为例,若数据要先进行前期处理,才可以开始正式操作,那么线程甲则需等待线程乙完成并且触发事件,其中最基本的方式是条件变量。按照“条件变量”的概念,若条件变量与某一事件或某一条件关联,一个或多个线程就能以其为依托,等待条件成立。当某线程判定条件成立时,就通过该条件变量,知会所有等待的线程,唤醒它们继续处理

💡 实际中我们一般只用方式三

condition_variable

头文件<condition_variable>

  • condition_variable
  • condition_variable_any

相同点:两者都能与std::mutex一起使用

不同点:前者仅限于与 std::mutex 一起工作,而后者可以和任何满足最低标准的互斥量一起工作,从而加上了_any的后缀,condition_variable_any会产生额外的开销

一般只推荐使用condition_variable,除非对灵活性有硬性要求,才会考虑condition_variable_any

条件变量的构造函数:

std::condition_variable::condition_variable
constructor:
    condition_variable();   //默认构造函数无参
    condition_variable(const condition_variable&) = delete;   //删除拷贝构造函数

示例代码:

#include <iostream>                // std::cout
#include <thread>                // std::thread
#include <mutex>                // std::mutex, std::unique_lock
#include <condition_variable>    // std::condition_variable

std::mutex mtx; // 全局互斥锁.
std::condition_variable cv; // 全局条件变量.
bool ready = false; // 全局标志位.

void do_print_id(int id)
{
    std::unique_lock <std::mutex> lck(mtx);
    while (!ready) // 如果标志位不为 true, 则等待...
        cv.wait(lck); // 当前线程被阻塞, 当全局标志位变为 true 之后,
    // 线程被唤醒, 继续往下执行打印线程编号id.
    std::cout << "thread " << id << '\n';
}

void go()
{
    std::unique_lock <std::mutex> lck(mtx);
    ready = true; // 设置全局标志位为 true.
    cv.notify_all(); // 唤醒所有线程.
}

int main()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(do_print_id, i);

    std::cout << "10 threads ready to race...\n";
    go(); // go!

  for (auto & th:threads)
        th.join();

    return 0;
}

打印输出:

10 threads ready to race...
thread 6
thread 7
thread 8
thread 9
thread 5
thread 4
thread 3
thread 2
thread 1
thread 0

wait( )与notify_one( )

wait( )函数

void wait( std::unique_lock<std::mutex>& lock );

//Predicate是lambda表达式
template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );
//以上二者都被notify_one())或notify_all()唤醒,但是
//第二种方式是唤醒后也要满足Predicate的条件
//如果不满足条件,继续解锁互斥量,然后让线程处于阻塞或等待状态
//第二种等价于
while (!pred())
{
    wait(lock);
}

std::condition_variable 提供了两种 wait() 函数。当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁(mutex),不妨设获得锁 lck),直到另外某个线程调用 notify_* 唤醒了当前线程

在线程被阻塞时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行
另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_* 唤醒了当前线程),wait() 函数也是自动调用 lck.lock( ),使得 lck 的状态和 wait 函数被调用时相同

  • notify_one 唤醒等待的一个线程,注意只唤醒一个
  • notify_all 唤醒所有等待的线程,使用该函数时应避免出现惊群效应

wait( )函数在第二种情况下(即设置了 Predicate),只有当 pred 条件为 false 时调用 wait( ) 才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred 为 true 时才会被解除阻塞

示例代码:

//  condition_variable simple example
//  Created by lei on 2022/05/15

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
using namespace std;

vector<int> data_vec;
int vec_size = 10;
bool prepared = false;
bool processed = false;

mutex m_mutex;
condition_variable m_cond_var;

void prepareData()
{
    data_vec.reserve(vec_size);
    for (int i = 0; i < vec_size; ++i)
    {
        data_vec.emplace_back(i + 1);
    }
}

void processData()
{
    for (int i = 0; i < vec_size; ++i)
    {
        data_vec[i] *= 2;
    }
}

void showData()
{
    for (int i = 0; i < vec_size; ++i)
    {
        cout << data_vec[i] << endl;
    }
}

void work()
{
    unique_lock<mutex> lk(m_mutex);
    m_cond_var.wait(lk, []
                    { return prepared; });
    cout << "Work thread is processing data..." << endl;
    processData();
    this_thread::sleep_for(chrono::seconds(2));
    processed = true;
    lk.unlock();
    m_cond_var.notify_one();
}

// use producer-consumer pattern
int main()
{
    thread worker(work);

    {
        lock_guard<mutex> lk(m_mutex);
        cout << "Preparing data..." << endl;
        prepareData();
        cout << "Before process:" << endl;
        showData();
        prepared = true;
    }
    m_cond_var.notify_one();

    {
        unique_lock<mutex> lk(m_mutex);
        m_cond_var.wait(lk, []
                        { return processed; });
        cout << "After process:" << endl;
        showData();
    }

    if (worker.joinable())
    {
        worker.join();
    }

    return 0;
}

打印输出:

Preparing data...
Before process:
1
2
3
4
5
6
7
8
9
10
Work thread is processing data...
After process:
2
4
6
8
10
12
14
16
18
20

spurious wakeups(虚假唤醒)

需要注意的一点是, wait有时会在没有任何线程调用notify的情况下返回,这种情况就是有名的spurious wakeup

这种情况会出现在第二种wait函数中,即:

template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );

spurious wake产生的原因就是当前置判断条件pred为true时,即使没有线程notify,只要锁已经被释放,wait函数就会立刻返回,继续执行下面的代码

如将上面的示例程序稍加修改:

// use producer-consumer pattern
int main()
{
    thread worker(work);

    {
        lock_guard<mutex> lk(m_mutex);
        cout << "Preparing data..." << endl;
        prepareData();
        cout << "Before process:" << endl;
        showData();
        prepared = true;
        this_thread::sleep_for(chrono::seconds(5));
    }
    // m_cond_var.notify_one();

主函数休眠5秒后释放锁,worker线程依旧可以继续执行

所以在实际使用中我们要添加一些判断条件尽量避免虚假唤醒的出现

参考博客

C++ 条件变量(condition_variable)

C++11多线程-条件变量(std::condition_variable)

C++11(六) 条件变量(condition_variable)

C++11 并发指南五(std::condition_variable 详解)

C++ std::condition_variable wait() wait_for() 区别 怎么用 实例

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: notify_one()函数用于唤醒一个正在等待condition_variable的线程,但是要注意,它只能唤醒一个线程,如果多个线程都在等待condition_variable,那么notify_one()只能唤醒其中一个线程,其他线程仍然处于等待状态。 ### 回答2: condition_variable 中的 notify_one() 函数用于通知一个正在等待的线程,使其从等待状态转变为可执行状态。在使用 notify_one() 函数时,需要注意以下几点: 1. notify_one() 必须在拥有 unique_lock 的情况下调用,以防止出现竞争条件(race condition)。唤醒线程和修改共享数据应该在同一个互斥锁中完成,这样可以确保在线程重新进入等待状态之前,共享数据已经正确地被修改。 2. 调用 notify_one() 后,无法保证哪个线程将被唤醒。有可能唤醒的是最早进入等待队列的线程,也有可能唤醒的是最新进入等待队列的线程。因此,在设计多线程程序时,不应该依赖于某个特定线程被唤醒的顺序。 3. 如果没有等待的线程,调用 notify_one() 不会有任何作用。因此,在使用 conditional_variable 时,需要结合判断条件来决定是否需要调用 notify_one(),以避免不必要的调用。 4. 在调用 notify_one() 后,被唤醒的线程并不会立即执行。线程仍然需要等待互斥锁解锁后,才能抢占到 CPU 资源并开始执行。 总之,notify_one() 是 condition_variable 类中用于唤醒一个正在等待的线程的函数。当调用 notify_one() 时,需注意使用 unique_lock 来保证互斥访问,并在必要时结合判断条件来决定是否调用 notify_one()。此外,调用 notify_one() 后并不能保证唤醒的线程会立即执行,线程仍需要等待互斥锁的解锁。 ### 回答3: condition_variable 中的 notify_one() 方法用于通知一个等待中的线程,告诉它可以继续执行了。下面是一些使用该方法时需要注意的事项: 1. notify_one() 方法必须在互斥锁保护下调用。在调用 notify_one() 方法之前,必须先获得相应的互斥锁,以确保线程安全性。 2. 在调用 notify_one() 方法之后,必须手动释放互斥锁,以便被通知的线程可以重新获得锁并执行。这样做是为了防止其他等待线程在 notify_one() 调用之后立即抢占互斥锁。 3. notify_one() 方法只会随机选择一个等待线程进行通知,如果有多个等待线程,则只有一个线程会被通知。因此,如果有多个等待线程,我们不能假设通知的是特定的线程。 4. notify_one() 方法通知的线程可能不会立即执行,它只是告诉该线程可以继续执行。线程的执行还受到操作系统的调度和优先级等因素的影响。 5. 当 notify_one() 方法调用之后,被通知的线程必须等待主动获得互斥锁之后才能继续执行。因此,被通知的线程通常会在获得锁之后再检查等待条件是否满足,如果不满足则继续等待。 总之,使用 condition_variablenotify_one() 方法时,我们必须保持互斥锁的正确使用,并要考虑线程调度的因素。同时,要注意多个等待线程中只有一个会被通知,被通知的线程还需再次检查等待条件是否满足。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Prejudices

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

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

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

打赏作者

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

抵扣说明:

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

余额充值