机制:
wait 函数之前需要先对条件变量的保护mutex 加锁。
当调用wait时,会依次进行如下三个动作:
1)wait会把自己的判断条件注册给内核(如果使用void wait( std::unique_lock<std::mutex>& lock, Predicate stop_waiting ););
2)wait会释放刚才持有的 保护mutex ;
3)接着阻塞住,等待被 notify 通知或者等待条件满足。如果调用的是带谓词的wait,那么在wait之前谓词就已经满足,则不需要等待被notify也能直接运行下去。如果调用的是不带谓词的wait,那么只能被notify来跳出wait。
当notify 发出通知时:
1)内核通知wait条件已经满足,但是wait还不会返回。
当notify线程notify后并解锁保护mutex时:
1)各个wait线程的wait函数在内部竞争保护mutex;
2)竞争到mutex的wait线程的wait函数返回。
当前线程的wait能够返回要同时满足三个条件:1)notify的通知到达了;2)notify的线程释放了保护mutex;3)当前线程在wait内部竞争到了保护mutex。
注:如果notify_all,则所有wait线程会依次获得锁,所谓的依次是上一个wait线程释放了保护mutex的时候,剩下的继续竞争,直到所有的wait线程都从wait返回或者因为wait条件无法得到满足而再次wait阻塞。
nofity解析:
notify_one的原语是通知一个 wait线程跳出wait状态,所有线程第一时间会竞争抢夺锁的控制权,获得控制权的线程有权利继续下一步,其他线程继续投入wait睡眠。
notify_all的原语是通知所有 wait线程跳出wait状态,如果是带谓词的wait则需要判断谓词是否满足,如果不满足则投入wait睡眠;如果是不带谓词的wait那么所有wait线程都会跳出wait继续运行。
notify_one 场景1:
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <stdlib.h>
#include <unistd.h>
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
void worker_thread()
{
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return ready;});
std::cout << "wait retain lock\n";
}
int main()
{
std::thread worker(worker_thread);
sleep(2); //make sure worker_thread step into wait statement
{
//due to wait will release mutex, so here lock will success.
std::lock_guard<std::mutex> lk(m);
//prepare condition
ready = true;
//notify wait thread
cv.notify_one();
std::cout << "wait for 5s, \n";
/*hold mutex m for 5 seconds, this will block the wait for executing, so even though condition is meet, but wait will still blocked for 5 seconds*/
sleep(5);
}
worker.join();
}
上面的例子中,wait的条件已经被满足了,但是worker thread还是会等待5秒后才跳出wait语句,因为main thread 拿着 mutex m sleep 了5 秒,这个时候 worker thread的wait语句实际上在等待 mutex m 的释放等了5 秒。
记住,wait线程进入wait语句的下一条语句时,是持有保护mutex的。获取动作在wait内部执行。
notify_one 场景2:
#include <thread>
#include <mutex>
#include <list>
#include <unistd.h>
#include <stdio.h>
#include <condition_variable>
#include <sys/types.h>
std::list<long> FIFO;
std::mutex lock;
std::condition_variable cv;
long consumer_v = -1;
long producer_v = 99999;
void consumer(){
static long times=0;
while(consumer_v!=0){
std::unique_lock<std::mutex> lk(lock);
cv.wait(lk,[]{return !FIFO.empty();});
consumer_v = std::move(FIFO.front());
FIFO.pop_front();
lk.unlock();
times++;
printf("[%d]consumer times : %ld\n" ,gettid(), times);
}
}
void producer(){
static long times=0;
while(producer_v--!=0){
sleep(5);
std::unique_lock<std::mutex> lk(lock);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
cv.notify_one();
lk.unlock();
times++;
printf("producer times : %ld\n" , times);
}
}
int main()
{
std::thread cons(consumer);
std::thread cons1(consumer);
std::thread prod(producer);
cons.join();
prod.join();
}
上述代码的输出为:
producer times : 1
[2042078]consumer times : 1
[2042078]consumer times : 2
[2042078]consumer times : 3
[2042078]consumer times : 4
[2042078]consumer times : 5
[2042078]consumer times : 6
可以看到,同一个消费者线程连续获得了6次机会,这说明带谓词的wait只要谓词能够满足则不需要等待notify。这里另外一个消费者线程因为没有抢到notify_one的消息而继续处于睡眠状态,进而丧失了6次资源争夺机会。
notify_one 场景3:
#include <thread>
#include <mutex>
#include <list>
#include <unistd.h>
#include <stdio.h>
#include <condition_variable>
#include <sys/types.h>
std::list<long> FIFO;
std::mutex lock;
std::condition_variable cv;
long consumer_v = -1;
long producer_v = 99999;
void consumer(){
static long times=0;
while(consumer_v!=0){
std::unique_lock<std::mutex> lk(lock);
cv.wait(lk,[]{return !FIFO.empty();});
consumer_v = std::move(FIFO.front());
FIFO.pop_front();
lk.unlock();
times++;
printf("[%d]consumer times : %ld\n" ,gettid(), times);
}
}
void producer(){
static long times=0;
while(producer_v--!=0){
sleep(5);
std::unique_lock<std::mutex> lk(lock);
FIFO.push_back(producer_v);
cv.notify_one();
FIFO.push_back(producer_v);
cv.notify_one();
FIFO.push_back(producer_v);
cv.notify_one();
FIFO.push_back(producer_v);
cv.notify_one();
FIFO.push_back(producer_v);
cv.notify_one();
FIFO.push_back(producer_v);
cv.notify_one();
lk.unlock();
times++;
printf("producer times : %ld\n" , times);
}
}
int main()
{
std::thread cons(consumer);
std::thread cons1(consumer);
std::thread prod(producer);
cons.join();
prod.join();
}
上述代码的输出为:
producer times : 1
[2044302]consumer times : 1
[2044303]consumer times : 2
[2044303]consumer times : 3
[2044303]consumer times : 4
[2044303]consumer times : 6
[2044302]consumer times : 5
这一次,两个消费者线程都得到了跳出wait的机会,这是因为进行了6次notify_one,每个线程都有6次机会跳出wait函数且继续往下运行。
notify_all:
#include <thread>
#include <mutex>
#include <list>
#include <unistd.h>
#include <stdio.h>
#include <condition_variable>
#include <sys/types.h>
std::list<long> FIFO;
std::mutex lock;
std::condition_variable cv;
long consumer_v = -1;
long producer_v = 99999;
void consumer(){
static long times=0;
while(consumer_v!=0){
std::unique_lock<std::mutex> lk(lock);
cv.wait(lk,[]{return !FIFO.empty();});
consumer_v = std::move(FIFO.front());
FIFO.pop_front();
lk.unlock();
times++;
printf("[%d]consumer times : %ld\n" ,gettid(), times);
}
}
void producer(){
static long times=0;
while(producer_v--!=0){
sleep(10);
std::unique_lock<std::mutex> lk(lock);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
cv.notify_all();
lk.unlock();
times++;
printf("producer times : %ld\n" , times);
}
}
int main()
{
std::thread cons(consumer);
std::thread cons1(consumer);
std::thread prod(producer);
cons.join();
prod.join();
}
从如下结果可以看到,当有充足的资源时,notify_all会让所有wait线程都有机会跳出wait等待状态。如果改成notify_one,则只会有一个线程有机会跳出wait等待。
producer times : 1
[2041004]consumer times : 1
[2041003]consumer times : 2
[2041003]consumer times : 4
[2041003]consumer times : 5
[2041003]consumer times : 6
[2041004]consumer times : 3
notify_all会同时唤醒所有wait的线程,但是如果想让所有线程都能执行下去而不是再次投入睡眠,则需要notify_all的时候,每个线程的wait条件能够得到满足,比如有2个消费者线程同时wait,而队列中只有一条数据,这个时候只有一个线程会在被唤醒后继续执行下去,另外一个在被唤醒后会因为队列为空而再次被投入睡眠。
wait解析:
有谓词的wait 和 没有谓词的wait 会导致两种完全不同的代码逻辑,一定不能将他们等效对待。上一章节的notify_one场景2中,如果wait都不指定谓词,那么wait是没有机会连续进入多次的。
#include <thread>
#include <mutex>
#include <list>
#include <unistd.h>
#include <stdio.h>
#include <condition_variable>
#include <sys/types.h>
std::list<long> FIFO;
std::mutex lock;
std::condition_variable cv;
long consumer_v = -1;
long producer_v = 99999;
void consumer(){
static long times=0;
while(consumer_v!=0){
std::unique_lock<std::mutex> lk(lock);
cv.wait(lk);
consumer_v = std::move(FIFO.front());
FIFO.pop_front();
lk.unlock();
times++;
printf("[%d]consumer times : %ld\n" ,gettid(), times);
}
}
void producer(){
static long times=0;
while(producer_v--!=0){
sleep(5);
std::unique_lock<std::mutex> lk(lock);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
FIFO.push_back(producer_v);
cv.notify_one();
lk.unlock();
times++;
printf("producer times : %ld\n" , times);
}
}
int main()
{
std::thread cons(consumer);
std::thread cons1(consumer);
std::thread prod(producer);
cons.join();
prod.join();
}
运行结果:
producer times : 1
[2045968]consumer times : 1
注意:
- 条件变量的信号不会排队,如果发送信号的一方在发送信号时还没有任何线程通过 wait 等待信号,那么这次信号会被丢弃(类比Windows下Event的脉冲模式,与之相反的是Windows下Event的电平模式)。
- wait 跳出阻塞是并不 100% 保证条件已经满足,我们还需要二次确认。因为有时候会出现spurious wakeup。所以最完美的写法是如下两种之一:
while (!stop_waiting()) {
wait(lock);
}
或者
wait(lock,[]{return stop_waiting()});
- wait 有其他两个版本,一个是 wait_for,一个是 wait_until ,二者都是用来附加一个超时机制,当超时时间到达则会跳出 wait 阻塞,这个时候我们需要人为判断是不是条件得到满足了。
- 当wait条件满足的时候,wait跳出阻塞向下执行(在wait执行完毕并进入下一条语句之前,wait会再次持有锁,但是在wait等待期间并不会持有锁),此时wait所在的那个线程是持有锁的,发送信号线程如果再次对锁加锁将被阻塞住。
- notify_one不会阻塞也不要求必须加锁才能使用,发送信号的线程只会在试图加锁时被阻塞。但是,如果加锁后再使用notify可以一定程度上提高效率。
代码片段:
std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;
void data_preparation_thread()
{
while (more_data_to_prepare())
{
data_chunk const data = prepare_data();
std::lock_guard<std::mutex> lk(mut);
data_queue.push(data);
data_cond.notify_one();
}
}
void data_processing_thread()
{
//下面两种场景是等同的,这里使用unique_lock的目的是让data_preparation_thread不在mut的获取上长时间阻塞,因为process(data);语句可能会非常耗时间
//如果不想用unique_lock,可以使用 {} 增加一个作用域来 人为触发 lock_guard 的析构,从而在 process(data); 之前释放 mut
#if 1
while (true)
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [] {return !data_queue.empty(); });
data_chunk data = data_queue.front();
data_queue.pop();
lk.unlock();
process(data);
if (is_last_chunk(data))
break;
}
#else
while (true)
{
{
std::lock_guard<std::mutex> lk(mut);
data_cond.wait(lk, [] {return !data_queue.empty(); });
data_chunk data = data_queue.front();
data_queue.pop();
}
process(data);
if (is_last_chunk(data))
break;
}
#endif
}
使用条件变量的线程安全队列:
#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>
template<typename T>
class threadsafe_queue
{
private:
mutable std::mutex mut;
std::queue<T> data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue()
{}
threadsafe_queue(threadsafe_queue const& other)
{
std::lock_guard<std::mutex> lk(other.mut);
data_queue = other.data_queue;
}
void push(T new_value)
{
std::lock_guard<std::mutex> lk(mut);
data_queue.push(new_value);
data_cond.notify_one();
}
void wait_and_pop(T& value)
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [this] {return !data_queue.empty(); });
value = data_queue.front();
data_queue.pop();
}
std::shared_ptr<T> wait_and_pop()
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [this] {return !data_queue.empty(); });
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return res;
}
bool try_pop(T& value)
{
std::lock_guard<std::mutex> lk(mut);
if (data_queue.empty())
return false;
value = data_queue.front();
data_queue.pop();
return true;
}
std::shared_ptr<T> try_pop()
{
std::lock_guard<std::mutex> lk(mut);
if (data_queue.empty())
return std::shared_ptr<T>();
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return res;
}
bool empty() const
{
std::lock_guard<std::mutex> lk(mut);
return data_queue.empty();
}
};
模拟WINDOWS的Event:
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <memory>
#include <thread>
#include <Windows.h>
std::mutex m;
std::condition_variable cv;
void func1() {
while (1) {
std::unique_lock<std::mutex> l(m);
cv.wait(l);
std::cout << "func1"<<std::endl;
}
}
void func2() {
while (1) {
std::unique_lock<std::mutex> l(m);
cv.wait(l);
std::cout << "func2" << std::endl;
}
}
void func3() {
while (1) {
std::unique_lock<std::mutex> l(m);
cv.wait(l);
std::cout << "func3" << std::endl;
}
}
int main()
{
std::thread t1(func1);
std::thread t2(func2);
std::thread t3(func3);
while (1) {
std::unique_lock<std::mutex> l(m);
cv.notify_all();
l.unlock();
Sleep(1000);
std::cout << "================\n";
}
std::cout << "Hello World!\n";
while (1);
}