目录

等待事件或其他条件

用条件变量等待条件

使用future等待一次性事件

从后台任务中返回值

std::future

std::packaged_task

std::promise

std::async

等待自多个线程

有时间限制的等待


有时候你不只是需要保护数据,还需要在独立的线程上进行同步操作。例如,一个线程在能够完成其任务之前可能需要等待另外一个线程完成任务。一般来说,希望一个线程等待特定的事件的发生或者是一个条件变为true是常见的事情。虽然通过定期检查“任务完成”的标识或是在共享数据中存储类似的东西也能做到这一点,但是不甚理想。对于像这样的线程间同步操作的需求是如此常见,以至于C++标准提供了条件变量(condition)和期值(future)为形式的工具来处理它。

等待事件或其他条件

如果一个线程正等待着第二个线程完成一项任务他有几个选择:

第一:第一个线程一直检查共享数据(由互斥元保护)中的标识,并且让第二个线程在完成任务时设置该标识。

第二:选择使用std::this_thread::sleep_for()函数,参数时毫秒。让等待中的线程在检查之间休眠一会儿。

第三:同时也是首选,是使用C++标准库提供的工具来等待事件本身。等待由另外一个线程触发一个事件的最基本机制是条件变量。

从概念上说,条件变量与某些事件或其他条件相关,并且一个或多个线程可以等待该条件被满足。当某个线程已经确定条件得到满足,他就可以通知一个或者多个正在条件变量上进行等待的线程,以便唤醒他们并继续处理。

用条件变量等待条件

标准C++库提供了两个条件变量的实现std::condition_variable和std::condition_variable_any。这两个实现都在<condition_variable>库的头文件中两者都需要和互斥元一起工作,以便提供恰当的同步;前者仅限于和std::mutex一起工作,而后者则可以与符合成为类似互斥元的最低标准的任何东西一起工作,因此以_any为后缀。因为std::condition_variable_any更加普遍,所以会有大小、性能或者操作系统资源方面的形式的额外代价的可能,因为应该首选std::condition_variable,除非需要额外的灵活性。

当 std::condition_variable 对象的某个 wait 函数被调用的时候,它使用 std::unique_lock(通过 std::mutex) 来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒当前线程。

示例代码:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>using namespace std;
std::condition_variable cdv; //全局条件变量
mutex mtx; //全局互斥锁
bool ready = false; //全局标志位
void func()
{std::unique_lock<std::mutex> lck(mtx);while (!ready) //如果标识位不为true,则等待...{cdv.wait(lck);  // 当前线程被阻塞,当全局标识位变为true之后,线程被唤醒继续往下执行打印线程ID}cout << "thread ID = " << std::this_thread::get_id() << endl;    //打印当前线程的ID
}
int main()
{thread th[10];for (auto& t : th){t = thread(func);}   thread t2([]() {std::unique_lock<std::mutex> lck(mtx);for (int i = 0; i < 20; ++i){cout << "i = " << i << endl;}ready = true; //设置全局标识位为truecdv.notify_all();//唤醒所有线程});for (auto& t : th){if (t.joinable()){t.join();}}if (t2.joinable()){t2.join();}return 0;
}

执行结果:

要是不同标识位也是可以的

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>using namespace std;
std::condition_variable cdv; //全局条件变量
mutex mtx; //全局互斥锁
void func()
{std::unique_lock<std::mutex> lck(mtx);cdv.wait(lck); // 当前线程被阻塞,当全局标识位变为true之后,线程被唤醒继续往下执行打印线程IDcout << "thread ID = " << std::this_thread::get_id() << endl; //打印当前线程的ID
}
int main()
{thread th[10];for (auto& t : th){t = thread(func);}   thread t2([]() {std::unique_lock<std::mutex> lck(mtx);for (int i = 0; i < 20; ++i){cout << "i = " << i << endl;}       cdv.notify_all();//唤醒所有线程});for (auto& t : th){if (t.joinable()){t.join();}}if (t2.joinable()){t2.join();}return 0;
}

结果跟上面的是一样的。

分享一个不错的博客

C++11 并发指南五(std::condition_variable 详解) - Haippy - 博客园 (cnblogs.com)

使用future等待一次性事件

如果等待线程只打算等待一次,那么当条件为true时它就不会再等待这个条件变量了,条件变量未必是同步机制的最佳选择。如果所等待的条件是一个特定数据块的可用性时,这尤其正确。在这个场景中使用期值(future)可能会更合适。

C++标准库使用future为这类一次性事件建模。如果一个线程需要等待特定的一次性事件,那么就会获取一个future来代表这一事件。然后,该线程可以周期性地在这个future上等待一小段时间以检查事件是否发生,而在检查间隙执行其他任务。另外,它还可以去做另外一个任务,直到其所需的事件已经发生才继续进行,随后就等待future变为就绪(ready)。future可能会有与之相关的数据,或者可能没有。一旦事件已经发生(即future已变为就绪),future就无法复位。

C++标准库中有两类future,是由<future>库的头文件中声明的两个类模板实现的:

唯一 future(unique future是,std::future<>)和共享future(shared futures,std::shared_future<>).这两个类模板是参照std::unique_ptr和std::shared_ptr 建立的。std::future的实例是仅有的一个指向其关联事件的实例,而多个std::shared_future的实例则可以指向同一个事件。对于后者而言,所有实例将同时变为就绪,并且它们都可以访问所有与该事件相关的数据。这些关联的数据就是这两种future成为模板的原因;像std::unique_ptr 和 std::shared_ptr 一样,模板参数就是关联数据的类型。

std::future<void>和std::shared_future<void>模板特化应该用于无关联数据的场合。

虽然future被用于线程间通信,但是future对象本身却并不提供同步访问。如果多线程需要访问同一个future对象,它们必须通过互斥元或其他同步机制来保护访问。

从后台任务中返回值

假设你有个长期运行的计算,预期最终将得到一个结果但是现在你还不需要这个值。你可以启动一个线程来执行计算,但这也意味着你必须注意将结果传回来,因为std::thread并没有提供直接的机制来这样做。这就是std::async函数模板。同样声明在<future>头文件中。

在不需要立即得到结果的时候,你可以使用std::async来启动一个异步任务。std::async返回一个std::future对象,而不是给你一个std::thread对象让你在上面等待,std::future对象最终将持有函数的返回值。当你需要这个值时,只要在future上调用get(),线程就会阻塞直到future就绪,然后返回该值。

std::async是一个函数模板,会启动一个异步任务,最终返回一个std::future对象。在之前我们都是通过thread去创建一个子线程,但是如果我们要得到这个子线程所返回的结果,那么可能就需要用全局变量或者引用的方法来得到结果,这样或多或少都会不太方便,那么async这个函数就可以将得到的结果保存在future中,然后通过future来获取想要得到的结果。async比起thread来说可以对线程的创建又有了更好的控制,比如可以延迟创建。下面先介绍一下std::future, std::packaged_task, std::promise。

std::future

std::future是一个类模板,提供了一个访问异步操作的结果的机制。我们可以通过future_status去查询future的三种状态,分别是deferred(还未执行),ready(已经完成),timeout(执行超时),所以我们可以通过这个去查询异步操作的状态。future提供了一些函数比如get(),wait(),wait_for(),一般用get()来获取future所得到的结果,如果异步操作还没有结束,那么会在此等待异步操作的结束,并获取返回的结果。wait()只是在此等待异步操作的结束,并不能获得返回结果。wait_for()超时等待返回结果。

#include <iostream>
#include <thread>
#include <mutex>
#include <future>using namespace std;
int func(int i)
{return 10 + i;
}
int main()
{std::future<int> fu = std::async(std::launch::async, func, 6); cout << fu.get() << endl;return 0;
}

结果:

std::packaged_task

std::packaged_task是一个类模板,顾名思义是用来打包的,将一个可调用对象封装起来,然后可以将其的返回值传给future。std::packaged_task<函数返回类型(参数类型)> 变量名(函数名)。下面展示一下std::packaged_task()的简单用法,也可以将函数换成lambda表达式。

std::packaged_task<>将一个future绑定到一个函数或者可调用对象上。当std::packaged_task<>对象被调用时,它就调用相关联的函数或者可调用对象,并且让future就绪,将返回值作为关联数据存储。这可以被用作线程池的构件,或者其他任务管理模式,例如在每个任务自己的线程上运行,或者一个特定的后台线程按顺序运行所有任务。

如果一个大型操作可以分为许多包含子任务,其中每一个都可以封装在一个std::packaged_task<>实例中,然后将该实例传给任务调度器或线程池。这样就抽象出了任务的详细信息,调度程序仅需要处理std::packaged_task<>实例,而非各个函数。

std::packaged_task<>类模板的模板参数为函数签名,比如void()表示无参数无返回值的函数,或者像int(string&,double*)表示接受一个对string的非const引用和double指针并返回int的函数。当你构造std::packaged_task实例的时候,你必须传入一个函数或者可调用对象,它可以接受指定的参数并且返回指定的类型。类型无需严格匹配,你可以用一个接受int并返回float的函数构造std::packaged_task<double(double)>,因为这些类型是可以隐式转换的。

指定的函数签名的返回类型确定了从get_future()成员函数返回std::future<>的类型。

#include <iostream>
#include <thread>
#include <mutex>
#include <future>using namespace std;
int func(int i)
{return 10 + i;
}
int main()
{std::packaged_task<int(int)> pt(func);   //int(int)是因为返回值是int,函数参数也是int   //将函数打包起来std::future<int> fu = pt.get_future();  // 并将结果返回给futurestd::thread t(std::ref(pt), 1);std::cout << fu.get() << std::endl;std::cout << std::this_thread::get_id() << std::endl;t.join();return 0;
}

结果:

std::promise

std::promise是一个类模板,它的作用是在不同的线程中实现数据的同步,与future结合使用,也间接实现了future在不同线程间的同步。

promise还有一个函数是set_value_at_thread_exit()这个翻译一下就可以直到它的作用是当在这个线程执行结束的时候才会将future的状态设置为ready,而set_value()则直接将future的状态设置为ready。需要注意的是在使用的过程中不能多次set_value(),也不能多次get_future()和多次get(),因为一个promise对象只能和一个对象相关联,否则就会抛出异常。

#include <iostream>
#include <future>
#include <thread>int fun(int x, std::promise<int>& p) {x++;x *= 10;p.set_value(x);std::cout << std::this_thread::get_id() << std::endl;return x;
}
int main()
{std::promise<int> p;std::future<int> fu = p.get_future();        // 并将结果返回给futurestd::thread t(fun, 1, std::ref(p));std::cout << fu.get() << std::endl;          // 当promise还没有值的时候在此等待std::cout << std::this_thread::get_id() << std::endl;t.join();return 0;
}

std::async

       其实这个函数是对上面的对象的一个整合,async先将可调用对象封装起来,然后将其运行结果返回到promise中,这个过程就是一个面向future的一个过程,最终通过future.get()来得到结果。它的实现方法有两种,一种是std::launch::async,这个是直接创建线程,另一种是std::launch::deferred,这个是延迟创建线程(当遇到future.get或者future.wait的时候才会创建线程),这两个参数是std::async的第一个参数,如果没有使用这个两个参数,也就是第一个参数为空的话,那么第一个参数默认为std::launch::async | std::launch::deferred,这个就不可控了,由操作系统根据当时的运行环境来确定是当前创建线程还是延迟创建线程。那么std::async的第二个参数就是可调用对象的名称,第三个参数就是可调用对象的参数。 

#include <iostream>
#include <thread>
#include <mutex>
#include <future>using namespace std;
int func(int i)
{return 10 + i;
}
int main()
{std::future<int> fu = std::async(std::launch::async, func, 6); cout << fu.get() << endl;return 0;
}

等待自多个线程

如果需要多余一个线程等待同一个事件则需要使用std::shared_future来代替。

有时间限制的等待

前面介绍所有阻塞都会调用一个不确定的时间段,挂起线程直至等待的事件发生。在许多情况下是没问题的,但在某些情况下你会希望给等待时间加一个限制。

有两类可供指定的超时:一为基于时间段的超时,即等待一个指定的时间长度(例如30ms)或者绝对超时,即等到下一个时间点(例如2022.01.29 10:10:10)。

处理基于时间段超时的变量具有_for后缀,处理绝对超时的变量具有_until后缀。

例如:std::condition_variable具有两个重载版本的wait_for()成员函数和两个wait_unitil()成员函数。

系统的学习一下C++标准的多线程----同步并发操作相关推荐

  1. 嵌入式Linux系统编程学习之二十六多线程概述

    文章目录 一.多线程概述 二.线程分类 三.线程创建的Linux实现 一.多线程概述   进程是系统中程序执行和资源分配的基本单位.每个进程有自己的数据段.代码段和堆栈段,这就造成进程在进行切换等操作 ...

  2. Java基础学习总结(104)——多线程、并发、工具类相关的面试题

    线程的概念 线程是程序执行的最小单位,也是操作系统调度和分派CPU的最小单元,是进程中的一个实体,是进程中的实际运作单位.可以在一个进程中启动多个线程来完成不同的任务,这些线程共享该进程拥有的资源. ...

  3. c++多线程——同步并发

    条件变量 c++库提供两个条件变量的实现,std::condition_variable和std::condition_variable_any,二者均在<condition_variable& ...

  4. VC 多线程同步方式操作串口

    #include<windows.h> #include<iostream> using namespace std; DWORD WINAPI CommReceive(LPV ...

  5. python类库32[多线程同步Lock+RLock+Semaphore+Event]

    2019独角兽企业重金招聘Python工程师标准>>> 一 多线程同步 由于CPython的python解释器在单线程模式下执行,所以导致python的多线程在很多的时候并不能很好地 ...

  6. java线程同步机制有哪些_多线程同步机制包括哪些,java线程同步机制

    多线程同步机制包括哪些什么是多线程同步机制,多线程同步机制包括:1.临界段用于实现"独占占有":2.信号量用于跟踪有限的资源:3.互斥是核心对象,可以实现不同线程之间的" ...

  7. 嵌入式系统开发学习步骤(Linux高级编程学习顺序)

    2019独角兽企业重金招聘Python工程师标准>>> 嵌入式系统开发学习步骤(Linux高级编程学习顺序) 1.Linux 基础 安装Linux操作系统 Linux文件系统 Lin ...

  8. 5w字总结 Unix系统编程学习笔记(面试向)(Unix环境高级编程/Unix环境程序设计)

    文章目录 一.计算 C语言的数据表示与处理 计算 C语言的基本运算操作 内存表和符号表 类型转换 函数类型的分析 指令 复合指令 句法 函数 函数激活(Activation Record) 函数激活定 ...

  9. 嵌入式系统开发学习如何起步、如何深入?(转)

    学习有捷径吗?俺认为是有的,正确的道路就是捷径. 就好象是爬山,如果有导游图,那就能找到一条最正确的路线:如果没有导游图,自己瞎琢磨,东问西问,也未必能找到最佳的路线. 有时候回首前尘,会谓叹,要是当 ...

最新文章

  1. 一款 PO VO DTO 转换神器
  2. 从0梳理1场时间序列赛事!
  3. java实现对HDFS增删改查(CRUD)等操作
  4. pythontxt文件怎么读_python怎么读txt文件
  5. C++编程练习:抽象类——编写一个程序,计算三角形、正方形的面积,抽象出一个基类base。
  6. mysql 连接 中文_大佬们E语言连接MYSQL输出中文乱码怎么破
  7. matlab 16位灰度值转8位,在matlab中如何将灰度值为24位的转化为8?
  8. mysql explain 结果值介绍
  9. 台积电5nm生产线污染原因查明:不影响A15芯片量产
  10. spark学习-scala版写的SparkSQL程序读取Hbase表注册成表SQL查询
  11. 同步中心服务器,同步中心以非常慢的速度同步脱机文件 - Windows Server | Microsoft Docs...
  12. mysql运算中max计算_MySQL 聚合函数、运算符操作、约束
  13. Google 开源机器学习数据集可视化工具 Facets
  14. 初学JAVA随记——8bit(1byte)的取值范围是+127到—128
  15. 安装ssd后不识别网卡_群晖E10M20-T1:你以为它是张网卡,其实它还带俩SSD
  16. NLP关键词提取方法总结及实现
  17. AARRR模型(模型数据指标详解)
  18. java浮点数减法_浮点数的相关运算Java实现
  19. 【操作系统篇】第五篇——调度(概念,层次,调度时机,切换与过程,方式,评价指标)
  20. linux内核编译的实质

热门文章

  1. 电商直播的带货技巧有哪些?
  2. 企业微信小程序_集成腾讯地图实现精准定位考勤打卡
  3. 通过css设定无背景色
  4. 当代刑事诉讼模式的转型图景
  5. 像素位移_PixelLogic像素画教程:高级像素位移
  6. php采集节目单,电视节目预告
  7. .net 中用TopShelf 实现windows服务
  8. 趣味算法——城市天际线
  9. 使用核磁共振波谱仪运用于高分子材料的NMR成像技术
  10. 财务会计计算机实训报告总结,会计电脑账实训心得体会.doc