线程同步---条件变量
学习自《深入应用C++11 代码优化与工程级应用》
过程中许多不懂的点问了智谱清言
条件变量
条件变量是C++11提供的另外一个用于等待的同步机制,它能阻塞一个或多个线程,直到收到另一个线程发出的通知或超时,才会唤起当前阻塞的线程。条件变量需要和互斥量配合起来用。
C++11提供了两种条件变量:
condition_variable:
配合std::unique_lock<std::mutex>进行wait操作。
condition_variable_any:
和任意带有lock,unlock语义的mutex搭配使用,比较灵活,但效率比condition_variable差一些。
condition_variable_any比condition_variable更灵活,因为它更通用,对所有的锁都适用,而condition_variable性能更好。
条件变量的使用过程如下:
拥有条件变量的线程获取互斥量。
循环检测某个条件,如果条件不满足,则阻塞直到条件满足;如果条件满足,则向下进行。
某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或所有的等待线程。
---------
std::unique_lock:
它提供了对互斥量(如std::mutex)的灵活锁定策略。std::unique_lock对象是可移动的,但不是可复制的,这意味着你可以通过移动语义来转移所有权,但不能复制它。这意味着你不能使用复制构造函数或复制赋值函数来复制std::unique_lock对象,但可以通过移动构造函数或移动赋值函数来转移对象的所有权。
调用release后,unique_lock对象的owns_lock()将返回false
#include <iostream>
#include <mutex>std::mutex mtx;int main() {std::unique_lock<std::mutex> ul(mtx); // 锁定互斥量// 释放互斥量的所有权std::mutex* p_mutex = ul.release();// 此时 unique_lock 对象不再拥有互斥量if (ul.owns_lock()) {std::cout << "unique_lock still owns the mutex." << std::endl;} else {std::cout << "unique_lock no longer owns the mutex." << std::endl;}// 手动锁定互斥量p_mutex->lock();// ... 在这里执行一些操作 ...// 手动解锁互斥量p_mutex->unlock();return 0;
}
---------
学习std::condition_variable
std::condition_variable要求与std::unique_lock<std::mutex>或std::lock_guard<std::mutex>一起使用
在调用
wait()
系列函数时,必须确保互斥量已经被当前线程锁定。
std::condition_variable
的wait()
方法用于使当前线程进入等待状态,直到条件变量被通知,或者某个特定的条件成立。在调用wait()
时,线程必须已经锁定了与之关联的互斥量(通常是std::unique_lock
),并且wait()
会在内部解锁互斥量,然后在等待期间阻塞线程。一旦条件变量被通知,wait()
将重新锁定互斥量,并返回。
void wait(unique_lock<mutex>& __lock);
使当前线程进入等待状态,直到条件变量被通知或者虚假唤醒发生。
函数在开始等待之前会自动释放这个锁,这样其他线程就可以获取这个锁并可能更改条件变量的状态。当
wait
函数返回时(无论是因为条件变量被通知还是虚假唤醒),它将重新获取这个锁。当
wait函数
被调用时,以下步骤会发生:
- 当前线程释放
__lock
所持有的互斥量。- 当前线程进入等待状态,等待条件变量被其他线程通过
notify_one()
或notify_all()
唤醒。- 当线程被唤醒时,它重新获取互斥量,然后
wait
函数返回。
---------
虚假唤醒:
虚假唤醒(Spurious Wakeup)是指在多线程编程中,一个线程在等待某些条件时,即使条件尚未满足,线程也可能被唤醒。这种现象在基于条件变量的同步机制中是可能的,并且是预期之内的行为。
虚假唤醒之所以会发生,是因为操作系统和硬件层面的原因,如中断、信号处理、线程调度策略等。虚假唤醒不是错误,而是程序员在使用条件变量时必须考虑的一种可能性。
以下是一些处理虚假唤醒的方法:
1.循环检查条件: 当线程从
wait()
函数返回时,应该总是在一个循环中检查条件是否真正满足,而不是假设wait()
返回就一定意味着条件已经满足。这种模式通常称为“等待-通知循环”或“自旋锁”。示例代码:
std::unique_lock<std::mutex> lock(mtx); while (!condition) { // 使用 while 而不是 ifcv.wait(lock); // 可能会发生虚假唤醒 } // 条件满足,继续执行后续操作
2.使用条件变量的其他形式: C++11 及更高版本提供了
wait()
函数的重载版本,允许你传递一个lambda表达式或函数对象来检查条件,从而避免显式循环。例如:std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return condition; }); // 条件满足,继续执行后续操作
在这个例子中,即使发生了虚假唤醒,lambda 表达式会再次检查条件,直到条件真正满足。
虚假唤醒并不是问题,因为通过正确使用条件变量,你可以确保程序的正确性。记住,当使用条件变量时,总是应该使用循环来检查条件,即使文档中没有明确说明虚假唤醒的存在。这是编写健壮多线程代码的最佳实践。
---------------
void wait(unique_lock<mutex>& __lock, _Predicate __p)
谓词(predicate)作为参数。谓词是一个可调用的对象(例如,函数、函数对象或 lambda 表达式),它用于检查等待条件是否满足
在这个实现中,
wait
方法会执行以下步骤:
- 检查谓词
__p
是否返回true
。如果返回true
,则表示等待条件已经满足,线程可以继续执行。- 如果谓词返回
false
,则调用不带谓词的wait
方法,这将使线程进入等待状态,直到条件变量被另一个线程通过notify_one()
或notify_all()
方法通知,或者发生虚假唤醒。- 如果线程被唤醒(无论是由于通知还是虚假唤醒),它会再次检查谓词。如果谓词仍然返回
false
,线程将继续等待。
这种方法确保了即使在发生虚假唤醒的情况下,线程也不会继续执行,直到谓词条件真正满足。这是处理条件变量时避免竞态条件和确保线程安全的关键。
在使用条件变量时,始终使用带有谓词的
wait
方法是一种好的实践,因为它可以简化代码并减少错误。
---------
void notify_one() noexcept;
当
notify_one()
被调用时,它会随机选择一个正在等待的线程(如果有的话)并唤醒它。
noexcept
noexcept
是一个关键字,用于指定某个函数或对象构造函数不会抛出异常。如果在函数或构造函数声明中使用了noexcept
,则表示该函数承诺不会因为运行时异常而终止执行。
---------
为什么使用 noexcept?
使用
noexcept
有几个好处:
性能提升:某些操作(如异常处理)可能需要额外的代码来处理异常情况。如果函数承诺不会抛出异常,编译器可以优化代码,从而可能提高性能。
简化资源管理:对于析构函数和释放资源的函数,使用
noexcept
可以确保资源管理更加安全和高效。增强异常安全:通过明确哪些函数可能抛出异常,哪些函数不会,可以更好地设计程序的异常安全策略。
注意事项
- 如果你声明了一个函数为
noexcept
,但它抛出了异常,程序会调用std::terminate
来立即终止执行。- 在移动构造函数和移动赋值运算符中使用
noexcept
是一个好的实践,因为它们通常不应该抛出异常。