进程间通信:condition_variable && unique_lock(c++)

condition_variable(条件变量)

简介

互斥锁用于上锁,条件变量用于等待;
condition_variable类是一个同步原语,可以用来阻塞一个线程,或者同时阻塞多个线程,直到另外一个线程修改了条件(a shared variable: the condition)并且通知了condition_variable(notify).

修改条件,并通知

  1. 获取一把锁,通常使用std::lock_guard。
  2. 在锁锁定的状态下,修改条件。
  3. 调用notify_one 或者 notify_all,进行通知
// 示例代码
struct CondInfo {
	std::mutex m;
	std::condition_variable cv;
	bool ready;
} condInfo;

// modify the condition and notify
void ThreadFunc1() {
    {
    	std::lock_guard<std::mutex> lck(condInfo.m);
    	condInfo.ready = true;
    }
    condInfo.cv.notify_one();
}

tips: 就算condition(例子中的ready)是atomic,也一定要在锁(例子中的m)锁定下进行修改,用来保证正确的将变量的修改发布到等待线程。

等待

  1. 获取一把std::unique_lock(锁定同一把锁,用于保护shared variable the condition)。
  2. 检查条件,防止已经更新和通知。
  3. 执行wait、wait_for、wait_until,wait操作会释放锁,并挂起当前线程。
  4. 当条件变量被notify、超时或者虚假唤醒发生时,当前线程被唤醒,并锁定锁,当前线程需要再次检查条件,如果是虚假唤醒需要再次调用wait操作。
 struct CondInfo {
 	std::mutex m;
 	std::condition_variable cv;
 	bool ready;
 } condInfo;

// 示例代码
void ThreadFunc2()
{
	{
		std::unique_lock<std::mutex> lck(condInfo.m);
		while (!condInfo.ready) { // 2.检查条件;4.再次检查,如果条件未变化(虚假唤醒),再次投入wait
			condInfo.cv.wait(lck); // 3.执行wait
		}
		// 或者使用
		// condInfo.wait(lck, []{return condInfo.ready;]); // 等同上面操作
	}
	// balabala
}

虚假唤醒

结合查到的资料分为两大类:
1、没有收到notify_one、notify_all, wait操作返回。导致虚假唤醒。
2、收到notify_one或者notify_all,但是当上锁时(wait:解锁,挂起,被notify唤醒,然后会再上锁),条件已经不为真。

第一类:当wait时被信号中断返回等一些系统原因。
第二类:系统调度相关:
比如:存在一个共享队列(生产消费队列),并有下面几个线程

线程 作用
消费线程1 获取锁,移除队列一个元素(队列为空),释放锁。轮询进行这样的操作
消费线程2 尝试移除队列一个元素:首先获取锁,判断队列是否为空,若为空,则调用wait操作进行阻塞等,直到得到notify。
生产线程3 获取锁,向队列插入一个元素,然后释放锁,进行notify。

考虑下面这种场景:线程2阻塞再wait,线程3执行notify,当线程2由于notify得到cpu的调度,再进行上锁的时候,线程1已经完成了移除元素的操作,那么此时对于线程2就属于一个虚假唤醒(因为此时的队列还是为空)。

生产者&&消费者例子

#include 
#include 
#include 
#include 

struct CondInfo {
    std::mutex m;
    std::condition_variable cv;
    std::queue<int> q;
} condInfo;

void Consume()
{
    while (1) {
        {
            std::unique_lock<std::mutex> lck(condInfo.m);
            // 使用overlo的wait操作,等价于下面注释的while循环
            condInfo.cv.wait(lck, [](&condInfo){return !condInfo.q.empty();}); 
            //while (condInfo.q.empty()) {
            //    condInfo.cv.wait(lck);
            //}
            printf("Consume elem:%d\n", condInfo.q.front());
            condInfo.q.pop();
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(2000));
    }
}

void Produce()
{
    int i = 0;
    while (1) {
        {
            std::lock_guard<std::mutex> lockGuard(condInfo.m);
            condInfo.q.push(i);
            printf("Produce elem:%d\n", i);
            i++;
        }
        condInfo.cv.notify_one();
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }
}

int main()
{
    std::thread consumeThd(&Consume);
    std::thread produceThd(&Produce);
    consumeThd.join();
    produceThd.join();
    return 0;
}

unique_lock

主要看它的几个构造函数:

  1. unique_lock() noexcept
    构造一个unique_lock(不使用关联的mutex)
  2. unique_lock( unique_lock&& other ) noexcept
    移动构造函数,使用other初始化本身(和other的mutex关联),other则不和任何mutex关联。
  3. explicit unique_lock( mutex_type& m ); // 上述条件变量的例子就是使用该构造函数
    使用一个关联mutex进行构造,调用m.lock()给关联mutex上锁,如果当前线程已经拥有了这把锁(这把锁是递归锁时除外),则该行为为未定义。
  4. unique_lock( mutex_type& m, std::defer_lock_t t ) noexcept
    使用一个关联的mutex进行构造,不调用m.lock()进行上锁。
  5. unique_lock( mutex_type& m, std::try_to_lock_t t )
    使用一个关联的mutex进行构造,调用m.try_lock()进行尝试上锁。如果当前线程已经拥有了这把锁(这把锁是递归锁时除外),则该行为为未定义。
  6. unique_lock( mutex_type& m, std::adopt_lock_t t );
    使用一个关联的mutex进行构造,m已经被当前线程上锁

tips:unique_lock符合RAII,当超过作用域时,会进行析构(对持有的mutex进行解锁)

std::mutex m;
{
	std::unique_lock<std::mutex> lck(m); // 使用第3个构造函数,对m进行上锁
	// balabala
} // 超出作用域后,lck进行析构,对m进行解锁

你可能感兴趣的:(Interprocess,Communications)