等待一个时间或其他条件

在一个线程等待完成任务时,会有很多选择:

1. 它可以持续的检查共享数据标志(用于做保护工作的互斥量),直到另一个线程完成工作时对这个标志进行重设。缺点:资源浪费,开销大

2. 在等待线程的检查间隙,使用std::this_thread::sleep_for()进行周期性的间歇。 缺点:休眠时间抉择困难

bool flag;
std::mutex m; void wait_for_flag()
{
std::unique_lock<std::mutex> lk(m);
while(!flag)
{
lk.unlock(); // 1 解锁互斥量
std::this_thread::sleep_for(std::chrono::milliseconds()); // 2 休眠100ms
lk.lock(); // 3 再锁互斥量
}
}

3. 使用C++标准库提供的工具去等待事件的发生。通过另一线程触发等待事件的机制是最基本的唤醒方式,这种机制就称为“条件变量”。

C++标准库对条件变量有两套实现:std::condition_variablestd::condition_variable_any。这两个实现都包含在<condition_variable>头文件的声明中。两者都需要与一个互斥量一起才能工作(互斥量是为了同步)

std::condition_variable:只能与std::mutex一起工作,开销少

std::condition_variable_any:可以和任何满足最低标准的互斥量一起工作,开销大

std::condition_variable 提供两个重要的接口:notify_one()wait()。wait()可以让线程陷入休眠状态,notify_one()就是唤醒处于wait中的其中一个条件变量(可能当时有很多条件变量都处于wait状态)。

template<typename Predicate>
wait(std::unique_lock<std::mutex>& lk, Predicate pred)

wait()会去检查这些条件(通过调用所提供的函数),当条件满足(调用所提供的函数返回true)时返回。如果条件不满足(调用所提供的函数返回false),wait()函数将解锁互斥量,并且将这个线程置于阻塞或等待状态。另外一个线程调用notify_one()通知条件变量时,线程从睡眠状态中苏醒,重新获取互斥锁,并且再次检查条件是否满足。

std::condition_variable::wait的一个最小化实现:

template<typename Predicate>
void minimal_wait(std::unique_lock<std::mutex>& lk,Predicate pred){
while(!pred()){
lk.unlock();
lk.lock();
}
}

考虑一个生产者消费者模型:一个线程往队列中放入数据,一个线程往队列中取数据,取数据前需要判断一下队列中确实有数据,由于这个队列是线程间共享的,所以,需要使用互斥锁进行保护,一个线程在往队列添加数据的时候,另一个线程不能取,反之亦然。用互斥锁实现如下:

#include <iostream>
#include <deque>
#include <thread>
#include <mutex> std::deque<int> q;
std::mutex mu; void function_1() {
int count = ;
while (count > ) {
std::unique_lock<std::mutex> locker(mu);
q.push_front(count);
locker.unlock();
std::this_thread::sleep_for(std::chrono::seconds());
count--;
}
} void function_2() {
int data = ;
while ( data != ) {
std::unique_lock<std::mutex> locker(mu);
if (!q.empty()) {
data = q.back();
q.pop_back();
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
} else {
locker.unlock();
}
}
}
int main() {
std::thread t1(function_1);
std::thread t2(function_2);
t1.join();
t2.join();
return ;
}

问题在于,如果生产者的速度比较慢,代码中每隔1s才会有一次数据生产,这时消费者都要去获取锁-->判断队列里是否有数据-->释放锁,这个过程就是资源的浪费,无用功使得cpu占用率很高。

使用std::this_thread::sleep_for()来对代码进行改造:

void function_2() {
int data = ;
while ( data != ) {
std::unique_lock<std::mutex> locker(mu);
if (!q.empty()) {
data = q.back();
q.pop_back();
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
} else {
locker.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds());
}
}
}

这样可以减低cpu占用率,但问题在于在实际操作中如何选择休眠时间,太长或者太短都不好。

最后可以使用条件变量来对这个代码进行改造:

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable> std::deque<int> q;
std::mutex mu;
std::condition_variable cond; void function_1() {
int count = ;
while (count > ) {
std::unique_lock<std::mutex> locker(mu);
q.push_front(count);
locker.unlock();
cond.notify_one(); // Notify one waiting thread, if there is one.
std::this_thread::sleep_for(std::chrono::seconds());
count--;
}
} void function_2() {
int data = ;
while ( data != ) {
std::unique_lock<std::mutex> locker(mu);
cond.wait(locker, [](){ return !q.empty();} ); // Unlock mu and wait to be notified
data = q.back();
q.pop_back();
locker.unlock();
std::cout << "t2 got a value from t1: " << data << std::endl;
}
}
int main() {
std::thread t1(function_1);
std::thread t2(function_2);
t1.join();
t2.join();
return ;
}

需要注意的几点:

在配合条件变量使用锁时,使用std::unique_lock比std::lock_guard合适,因为在wait内部有对锁的unlock和lock操作

使用细粒度锁,尽量减小锁的范围,在notify_one()的时候,不需要处于互斥锁的保护范围内,所以在唤醒条件变量之前可以将锁unlock()

参考资料:

https://www.jianshu.com/p/c1dfa1d40f53

https://chenxiaowei.gitbook.io/c-concurrency-in-action-second-edition-2019/4.0-chinese/4.1-chinese

c++多线程并发学习笔记(2)的更多相关文章

  1. c++多线程并发学习笔记(0)

    多进程并发:将应用程序分为多个独立的进程,它们在同一时刻运行.如图所示,独立的进程可以通过进程间常规的通信渠道传递讯息(信号.套接字..文件.管道等等). 优点:1.操作系统在进程间提供附附加的保护操 ...

  2. c++多线程并发学习笔记(1)

    共享数据带来的问题:条件竞争 避免恶性条件竞争的方法: 1. 对数据结构采用某种保护机制,确保只有进行修改的线程才能看到修改时的中间状态.从其他访问线程的角度来看,修改不是已经完成了,就是还没开始. ...

  3. 多线程编程学习笔记——使用异步IO(一)

    接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...

  4. 多线程编程学习笔记——使用异步IO

    接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...

  5. Java多线程技术学习笔记(二)

    目录: 线程间的通信示例 等待唤醒机制 等待唤醒机制的优化 线程间通信经典问题:多生产者多消费者问题 多生产多消费问题的解决 JDK1.5之后的新加锁方式 多生产多消费问题的新解决办法 sleep和w ...

  6. 多线程编程学习笔记——async和await(一)

    接上文 多线程编程学习笔记——任务并行库(一) 接上文 多线程编程学习笔记——任务并行库(二) 接上文 多线程编程学习笔记——任务并行库(三) 接上文 多线程编程学习笔记——任务并行库(四) 通过前面 ...

  7. 多线程编程学习笔记——async和await(二)

    接上文 多线程编程学习笔记——async和await(一) 三.   对连续的异步任务使用await操作符 本示例学习如何阅读有多个await方法方法时,程序的实际流程是怎么样的,理解await的异步 ...

  8. 多线程编程学习笔记——async和await(三)

    接上文 多线程编程学习笔记——async和await(一) 接上文 多线程编程学习笔记——async和await(二) 五.   处理异步操作中的异常 本示例学习如何在异步函数中处理异常,学习如何对多 ...

  9. 多线程编程学习笔记——编写一个异步的HTTP服务器和客户端

    接上文 多线程编程学习笔记——使用异步IO 二.   编写一个异步的HTTP服务器和客户端 本节展示了如何编写一个简单的异步HTTP服务器. 1.程序代码如下. using System; using ...

随机推荐

  1. C#.Net集成Bartender条码打印,VS调试运行可以打印,发布到IIS运行打印报错

    C#.Net集成Bartender条码打印,VS调试运行可以打印,发布到IIS运行打印报错 问题原因: 问题出现在iis账户权限. 解决方法: iis默认是用network service这个账户去执 ...

  2. PHP基础-表达式介绍

    表达式是 PHP 最重要的基石.在PHP 编程 中,几乎所写的任何东西都是一个表达式.简单但却最精确的定义一个表达式的方式就是“任何有值的东西”. 最基本的表达式形式是常量和变量.当键入“$a = 5 ...

  3. noi 求分数序列和 x

    求分数序列和 总时间限制:  1000ms 内存限制:  65536kB 描述 有一个分数序列 q1/p1,q2/p2,q3/p3,q4/p4,q5/p5,.... ,其中qi+1= qi+ pi, ...

  4. BZOJ 1901 洛谷 P2617 ZOJ 2112 Dynamic Rankings

    以下时空限制来自zoj Time limit 10000 ms Memory limit 32768 kB OS Linux Source Online Contest of Christopher' ...

  5. jQuery_完成省市二级联动

    当填表的时候会让你设计某省某市怎么设计,应该明白,如果你选择了一个确定的省,那么在第二个下拉框内则不会有除了你选择的省的市之外的名称.而这功能用js来实现很麻烦,但是用jq确很容易实现. 原表结构: ...

  6. elasticsearch-head插件的安装

    2.4.1 安装nodejs Node.js是一个基于 Chrome V8 引擎的 JavaScript 运行环境. Node.js是一个Javascript运行环境(runtime environm ...

  7. hibernate多对一单项关联映射

    1.实体类编写: 用户类: public class User { private int id; private String name; private Group group; ..... } ...

  8. 20165218 《网络对抗技术》 Exp9 网络安全基础

    Exp9 网络完全基础 基础问题回答 SQL注入攻击原理,如何防御 所谓SQL注入,就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令. ...

  9. 第四周实验总结&实验报告

    实验二 Java简单类与对象 实验目的 掌握类的定义,熟悉属性.构造函数.方法的作用,掌握用类作为类型声明变量和方法返回值: 理解类和对象的区别,掌握构造函数的使用,熟悉通过对象名引用实例的方法和属性 ...

  10. @清晰掉 qsort()

    qsort函数描述: http://www.cnblogs.com/sooner/archive/2012/04/18/2455011.html qsort()函数实现: /*** *qsort.c ...