linux多线程——condition_variable条件变量

一、condition_variable 概述
<condition_variable>是C++标准程序库中的一个头文件,定义了C++11标准中的一些用于并发编程时表示条件变量的类与方法等。
条件变量是并发程序设计中的一种控制结构。多个线程访问一个共享资源(或称临界区)时,不但需要用互斥锁实现独享访问以避免并发错误(称为竞争危害)在获得互斥锁进入临界区后还需要检验特定条件是否成立
(1)、如果不满足该条件,拥有互斥锁的线程应该释放该互斥锁,把自身阻塞(block)并挂到(suspend)条件变量的线程队列中,如实例代码的子线程执行体调用条件变量的wait函数;
(2)、如果满足该条件,拥有互斥锁的线程在临界区内访问共享资源,在退出临界区时通知(notify)在条件变量的线程队列中处于阻塞状态的线程,被通知的线程必须重新申请对该互斥锁加锁。如实例代码中主线程调用go函数,先申请互斥锁来操作临界区,退出前调用条件变量的notify_one函数来通知子线程;
二、条件变量的使用
C++11的标准库中新增加的条件变量的实现,与pthread的实现语义完全一致。使用条件变量做并发控制时,某一时刻阻塞在一个条件变量上的各个线程应该在调用wait操作时指明同一个互斥锁,此时该条件变量与该互斥锁绑定;否则程序的行为未定义。条件变量必须与互斥锁配合使用,其理由是程序需要判定某个条件(condition或称predict)是否成立,该条件可以是任意复杂。
离开临界区的线程用notify操作解除阻塞(unblock)在条件变量上的各个线程时,按照公平性(fairness)这些线程应该有平等的获得互斥锁的机会,不应让某个线程始终难以获得互斥锁被饿死(starvation),并且比后来到临界区的其它线程更为优先(即基本上FIFO)。一种办法是调用了notify_all的线程保持互斥锁,直到所有从条件变量上解除阻塞的线程都已经挂起(suspend)到互斥锁上,然后发起了notify_all的线程再释放互斥锁。互斥锁上一般都有比较完善的阻塞线程调度算法,一般会按照线程优先级调度,相同优先级按照FIFO调度。
发起notify的线程不需要拥有互斥锁。即将离开临界区的线程是先释放互斥锁还是先notify操作解除在条件变量上挂起线程的阻塞?表面看两种顺序都可以。但一般建议是先notify操作,后对互斥锁解锁。因为这既有利于上述的公平性,同时还避免了相反顺序时可能的优先级倒置。这种先notify后解锁的做法是悲观的(pessimization),因为被通知(notified)线程将立即被阻塞,等待通知(notifying)线程释放互斥锁。很多实现(特别是pthreads的很多实现)为了避免这种”匆忙与等待”(hurry up and wait)情形,把在条件变量的线程队列上处于等待的被通知线程直接移到互斥锁的线程队列上,而不唤醒这些线程。
C++11中引入了条件变量,其相关内容均在<condition_variable>中。这里主要介绍std::condition_variable类。
条件变量std::condition_variable用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程。std::condition_variable需要与std::unique_lock配合使用。std::condition_variable效果上相当于包装了pthread库中的pthread_cond_*()系列的函数。
**当std::condition_variable对象的某个wait函数被调用的时候,它使用std::unique_lock(通过std::mutex)来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的std::condition_variable对象上调用了notification函数来唤醒当前线程。**实例代码中的条件变量cv;
std::condition_variable对象通常使用std::unique_lockstd::mutex来等待,如果需要使用另外的lockable类型,可以使用std::condition_variable_any类。
std::condition_variable类的成员函数:
(1)、构造函数:仅支持默认构造函数,拷贝、赋值和移动(move)均是被禁用的。
(2)、wait:当前线程调用wait()后将被阻塞,直到另外某个线程调用notify_*唤醒当前线程;当线程被阻塞时,该函数会自动调用std::mutex的unlock()释放锁,使得其它被阻塞在锁竞争上的线程得以继续执行。一旦当前线程获得通知(notify,通常是另外某个线程调用notify_*唤醒了当前线程),wait()函数也是自动调用std::mutex的lock()。wait分为无条件被阻塞和带条件的被阻塞两种。
无条件被阻塞:调用该函数前,当前线程应该已经对unique_lock lck完成了加锁(但是不能使用lock_guard,因为等待中的函数有可能锁定或解除mutex,但是lock_guard不提供lock()或unlock()函数,unique_lock()提供lock()或unlock()函数)。所有使用同一个条件变量的线程必须在wait函数中使用同一个unique_lock。该wait函数内部会自动调用lck.unlock()对互斥锁解锁,使得其他被阻塞在互斥锁上的线程恢复执行。使用本函数被阻塞的当前线程在获得通知(notified,通过别的线程调用 notify_*系列的函数)而被唤醒后,wait()函数恢复执行并自动调用lck.lock()对互斥锁加锁。
带条件的被阻塞:wait函数设置了谓词(Predicate),只有当pred条件为false时调用该wait函数才会阻塞当前线程,并且在收到其它线程的通知后只有当pred为true时才会被解除阻塞。因此,等效于while (!pred()) wait(lck).
(3)、wait_for:与wait()类似,只是wait_for可以指定一个时间段,在当前线程收到通知或者指定的时间超时之前,该线程都会处于阻塞状态。而一旦超时或者收到了其它线程的通知,wait_for返回,剩下的步骤和wait类似。
(4)、wait_until:与wait_for类似,只是wait_until可以指定一个时间点,在当前线程收到通知或者指定的时间点超时之前,该线程都会处于阻塞状态。而一旦超时或者收到了其它线程的通知,wait_until返回,剩下的处理步骤和wait类似。
(5)、notify_all: 唤醒所有的wait线程,如果当前没有等待线程,则该函数什么也不做。
(6)、notify_one:唤醒某个wait线程,如果当前没有等待线程,则该函数什么也不做;如果同时存在多个等待线程,则唤醒某个线程是不确定的(unspecified)。
条件变化存在虚假唤醒的情况,因此在线程被唤醒后需要检查条件是否满足。无论是notify_one或notify_all都是类似于发出脉冲信号,如果对wait的调用发生在notify之后是不会被唤醒的,所以接收者在使用wait等待之前也需要检查条件是否满足。
std::condition_variable_any类与std::condition_variable用法一样,区别仅在于std::condition_variable_any的wait函数可以接受任何lockable参数,而std::condition_variable只能接受std::unique_lockstd::mutex类型的参数。
std::notify_all_at_thread_exit函数:当调用该函数的线程退出时,所有在cond条件变量上等待的线程都会收到通知。
实例代码:

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

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

void thread_fun(int id)
{
	std::cout << "11111111" << std::endl;
    std::unique_lock <std::mutex> lck(mtx);
	std::cout << "777777777 lck = "<< &lck << std::endl;
	{
	   std::cout << "22222222 " << std::endl;
       cv.wait(lck); // 当前线程被阻塞, 当全局标志位变为 true 之后,
    // 线程被唤醒, 继续往下执行打印线程编号id.
	}
    std::cout << "thread " << id << '\n';
}

void go()
{
	std::cout << "3333333"  << std::endl;
    std::unique_lock <std::mutex> lck(mtx);
	std::cout << "4444444 lck=" << &lck << std::endl;
	std::cout << "5555555"  << std::endl;
    cv.notify_one(); // 唤醒所有线程.
	std::cout << "6666666"  << std::endl;
}

int main()
{
    std::thread threads[1];
    for (int i = 0; i < 1; ++i)
	{
        threads[i] = std::thread(thread_fun, i);
	}
	sleep(1);
    go(); 
	for (auto & th:threads)
	{
		th.join();
	}
	return 0;
}

执行结果:

11111111
777777777 lck = 0x7f642efb6e30
22222222 
3333333
4444444 lck=0x7fff49341100
5555555
6666666
thread 0

从执行结果我们可以看到
(1)主函数创建一个线程(一个线程比较容易分析)并绑定线程执行体thread_fun,线程执行体内调用条件变量的wait函数将被阻塞,直到主线程调用notify_one唤醒;
(2)主线程调用go函数并唤醒noetify_one()条件变量cv的线程;
工作中应用场景:
(1)TCP服务器有一个线程池用于TCP连接请求处理业务,开启多个线程处理业务,但是具体是哪一个线程来执行就需要条件变量来控制;
(2)将TCP连接请求push到请求队列,同时使用条件变量通知TCP服务器的线程池来处理请求;
参考:
https://blog.csdn.net/fengbingchun/article/details/73695596
https://blog.csdn.net/qq_41453285/article/details/105605310

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值