Effective Modern C++ 第七章 并发API 1

这篇具有很好参考价值的文章主要介绍了Effective Modern C++ 第七章 并发API 1。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

条款35:优先使用基于任务而非基于线程的程序设计

要点速记:

 条款36:如果异步是必要的,则指定std::launch::async

要点速记:

参考:EffectiveModernCppChinese/src/7.TheConcurrencyAPI/Item35.md at master · CnTransGroup/EffectiveModernCppChinese (github.com)

条款35:优先使用基于任务而非基于线程的程序设计

基于任务的方法通常比基于线程的方法更优,原因之一上面的代码已经表明,基于任务的方法代码量更少。我们假设调用doAsyncWork的代码对于其提供的返回值是有需求的。基于线程的方法对此无能为力,而基于任务的方法就简单了,因为std::async返回的future提供了get函数(从而可以获取返回值)。如果doAsycnWork发生了异常,get函数就显得更为重要,因为get函数可以提供抛出异常的访问,而基于线程的方法,如果doAsyncWork抛出了异常,程序会直接终止(通过调用std::terminate)。

基于线程与基于任务最根本的区别在于,基于任务的抽象层次更高。基于任务的方式使得开发者从线程管理的细节中解放出来,对此在C++并发软件中总结了“thread”的三种含义:

  • 硬件线程(hardware threads)是真实执行计算的线程。现代计算机体系结构为每个CPU核心提供一个或者多个硬件线程。
  • 软件线程(software threads)(也被称为系统线程(OS threads、system threads))是操作系统(假设有一个操作系统。有些嵌入式系统没有。)管理的在硬件线程上执行的线程。通常可以存在比硬件线程更多数量的软件线程,因为当软件线程被阻塞的时候(比如 I/O、同步锁或者条件变量),操作系统可以调度其他未阻塞的软件线程执行提供吞吐量。
  • std::thread 是C++执行过程的对象,并作为软件线程的句柄(handle)。有些std::thread对象代表“空”句柄,即没有对应软件线程,因为它们处在默认构造状态(即没有函数要执行);有些被移动走(移动到的std::thread就作为这个软件线程的句柄);有些被join(它们要运行的函数已经运行完);有些被detach(它们和对应的软件线程之间的连接关系被打断)。
auto fut = std::async(doAsyncWork); //线程管理责任交给了标准库的开发者

这种调用方式将线程管理的职责转交给C++标准库的开发者。举个例子,这种调用方式会减少抛出资源超额异常的可能性,因为这个调用可能不会开启一个新的线程。你会想:“怎么可能?如果我要求比系统可以提供的更多的软件线程,创建std::thread和调用std::async为什么会有区别?”确实有区别,因为以这种形式调用(即使用默认启动策略——见Item36)时,std::async不保证会创建新的软件线程。然而,他们允许通过调度器来将特定函数(本例中为doAsyncWork)运行在等待此函数结果的线程上(即在对fut调用get或者wait的线程上),合理的调度器在系统资源超额或者线程耗尽时就会利用这个自由度。

如果考虑自己实现“在等待结果的线程上运行输出结果的函数”,之前提到了可能引出负载不均衡的问题,这问题不那么容易解决,因为应该是std::async和运行时的调度程序来解决这个问题而不是你。遇到负载不均衡问题时,对机器内发生的事情,运行时调度程序比你有更全面的了解,因为它管理的是所有执行过程,而不仅仅个别开发者运行的代码。

有了std::async,GUI线程中响应变慢仍然是个问题,因为调度器并不知道你的哪个线程有高响应要求。这种情况下,你会想通过向std::async传递std::launch::async启动策略来保证想运行函数在不同的线程上执行

对比基于线程的编程方式,基于任务的设计为开发者避免了手动线程管理的痛苦,并且自然提供了一种获取异步执行程序的结果(即返回值或者异常)的方式。当然,仍然存在一些场景直接使用std::thread会更有优势:

  • 你需要访问非常基础的线程API。C++并发API通常是通过操作系统提供的系统级API(pthreads或者Windows threads)来实现的,系统级API通常会提供更加灵活的操作方式(举个例子,C++没有线程优先级和亲和性的概念)。为了提供对底层系统级线程API的访问,std::thread对象提供了native_handle的成员函数,而std::future(即std::async返回的东西)没有这种能力。
  • 你需要且能够优化应用的线程使用。举个例子,你要开发一款已知执行概况的服务器软件,部署在有固定硬件特性的机器上,作为唯一的关键进程。
  • 你需要实现C++并发API之外的线程技术,比如,C++实现中未支持的平台的线程池。

这些都是在应用开发中并不常见的例子,大多数情况,开发者应该优先采用基于任务的编程方式。

要点速记:

  • std::thread API不能直接访问异步执行的结果,如果执行函数有异常抛出,代码会终止执行。
  • 基于线程的编程方式需要手动的线程耗尽、资源超额、负责均衡、平台适配性管理。
  • 通过带有默认启动策略的std::async进行基于任务的编程方式会解决大部分问题。

 条款36:如果异步是必要的,则指定std::launch::async

当你调用std::async执行函数时(或者其他可调用对象),你通常希望异步执行函数。但是这并不一定是你要求std::async执行的操作。你事实上要求这个函数按照std::async启动策略来执行。有两种标准策略,每种都通过std::launch这个限域enum的一个枚举名表示(关于枚举的更多细节参见Item10)。假定一个函数f传给std::async来执行:

  • std::launch::async启动策略意味着f必须异步执行,即在不同的线程。
  • std::launch::deferred启动策略意味着f仅当在std::async返回的future上调用get或者wait时才执行。这表示f推迟到存在这样的调用时才执行(译者注:异步与并发是两个不同概念,这里侧重于惰性求值)。当getwait被调用,f会同步执行,即调用方被阻塞,直到f运行结束。如果getwait都没有被调用,f将不会被执行。(这是个简化说法。关键点不是要在其上调用getwait的那个future,而是future引用的那个共享状态。(Item38讨论了future与共享状态的关系。)因为std::future支持移动,也可以用来构造std::shared_future,并且因为std::shared_future可以被拷贝,对共享状态——对f传到的那个std::async进行调用产生的——进行引用的future对象,有可能与std::async返回的那个future对象不同。这非常绕口,所以经常回避这个事实,简称为在std::async返回的future上调用getwait。)

可能让人惊奇的是,std::async的默认启动策略——你不显式指定一个策略时它使用的那个——不是上面中任意一个。相反,是求或在一起的。下面的两种调用含义相同:

auto fut1 = std::async(f);                      //使用默认启动策略运行f
auto fut2 = std::async(std::launch::async |     //使用async或者deferred运行f
                       std::launch::deferred,
                       f);

 因此默认策略允许f异步或者同步执行。如同Item35中指出,这种灵活性允许std::async和标准库的线程管理组件承担线程创建和销毁的责任,避免资源超额,以及平衡负载。这就是使用std::async并发编程如此方便的原因。

但是,使用默认启动策略的std::async也有一些有趣的影响。给定一个线程t执行此语句:

auto fut = std::async(f);   //使用默认启动策略运行f
  • 无法预测f是否会与t并发运行,因为f可能被安排延迟运行。
  • 无法预测f是否会在与某线程相异的另一线程上执行,这个某线程在fut上调用getwait。如果对fut调用函数的线程是t,含义就是无法预测f是否在异于t的另一线程上执行。
  • 无法预测f是否执行,因为不能确保在程序每条路径上,都会不会在fut上调用get或者wait

默认启动策略的调度灵活性导致使用thread_local变量比较麻烦,因为这意味着如果f读写了线程本地存储thread-local storage,TLS),不可能预测到哪个线程的变量被访问。

这还会影响到基于wait的循环使用超时机制,因为在一个延时的任务(参见Item35)上调用wait_for或者wait_until会产生std::launch::deferred值。意味着,以下循环看似应该最终会终止,但可能实际上永远运行:

using namespace std::literals;      //为了使用C++14中的时间段后缀;参见条款34

void f()                            //f休眠1秒,然后返回
{
    std::this_thread::sleep_for(1s);
}

auto fut = std::async(f);           //异步运行f(理论上)

while (fut.wait_for(100ms) !=       //循环,直到f完成运行时停止...
       std::future_status::ready)   //但是有可能永远不会发生!
{
    …
}

这些各种考虑的结果就是,只要满足以下条件,std::async的默认启动策略就可以使用:

  • 任务不需要和执行getwait的线程并行执行。
  • 读写哪个线程的thread_local变量没什么问题。
  • 可以保证会在std::async返回的future上调用getwait,或者该任务可能永远不会执行也可以接受。
  • 使用wait_forwait_until编码时考虑到了延迟状态。

如果上述条件任何一个都满足不了,你可能想要保证std::async会安排任务进行真正的异步执行。进行此操作的方法是调用时,将std::launch::async作为第一个实参传递:文章来源地址https://www.toymoban.com/news/detail-728231.html

auto fut = std::async(std::launch::async, f);   //异步启动f的执行

要点速记:

  • std::async的默认启动策略是异步和同步执行兼有的。
  • 这个灵活性导致访问thread_locals的不确定性,隐含了任务可能不会被执行的意思,会影响调用基于超时的wait的程序逻辑。
  • 如果异步执行任务非常关键,则指定std::launch::async

到了这里,关于Effective Modern C++ 第七章 并发API 1的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 第七章金融中介

             金融中介是通过向资金盈余者发行 间接融资合约( 如存款单),并和资金短缺者达成 间接投资合约 (发放信贷)或购买其发行的证券,在资金供求方之间融通资金,对资金跨期、跨域进行优化配置的金融机构。         金融体系由金融市场和金融中介构成,以银行业为

    2024年02月04日
    浏览(59)
  • 第七章 函数矩阵

    和矩阵函数不同的是,函数矩阵本质上是一个矩阵,是以函数作为元素的矩阵。 矩阵函数本质上是一个矩阵,是以矩阵作为自变量的函数。 函数矩阵和数字矩阵的运算法则完全相同。 不过矩阵的元素 a i j ( x ) a_{ij}(x) a ij ​ ( x ) 需要是闭区间 [ a , b ] [a,b] [ a , b ] 上的实函数

    2024年02月04日
    浏览(63)
  • [JavaScript] 第七章 对象

    🌹作者主页:青花锁 🌹简介:Java领域优质创作者🏆、Java微服务架构公号作者😄 🌹简历模板、学习资料、面试题库、技术互助 🌹文末获取联系方式 📝 [Java项目实战] 介绍Java组件安装、使用;手写框架等 [Aws服务器实战] Aws Linux服务器上操作nginx、git、JDK、Vue等 [Java微服务

    2024年02月02日
    浏览(132)
  • 第七章 图论

    第七章 图论 一、数据结构定义 图的邻接矩阵存储法 图的邻接表存储法 把所有节点存储为节点数组,每个节点里有自己的数据和一个边指针,这个边指针相当于一个链表的头指针,这个链表里存放所有与这个节点相连的边,边里存放该边指向的节点编号和下一条边指针 图的

    2024年02月14日
    浏览(96)
  • 数据结构第七章

    图(Graph)G由两个集合V和E组成,记为G=(V, E),其中V是顶点的有穷非空集合,E是V中顶点偶对的有穷集合,这些顶点偶对称为边。V(G)和E(G)通常分别表示图G的顶点集合和边集合,E(G)可以为空集。若EG)为空,则图G只有顶点而没有边。 子图:假设有两个图G=(V,E)和G1=(V1,E1);如果V1

    2024年02月03日
    浏览(66)
  • 第七章 测试

    7.1.1 选择程序设计语言 1. 计算机程序设计语言基本上可以分为汇编语言和高级语言 2. 从应用特点看,高级语言可分为基础语言、结构化语言、专用语言 01 有理想的模块化机制; 02 可读性好的控制结构和数据结构; 03 便于调试和提高软件可靠性; 04 编译程序发现程序错误的

    2024年02月08日
    浏览(80)
  • python第七章(字典)

    一。字典(类型为dict)的特点: 1.符号为大括号 2.数据为键值对形式出现 3.各个键值对之间以逗号隔开 格式:str1={\\\'name\\\':\\\'Tom\\\'}  name相当于键值(key),Tom相当于值 二。空字典的创建方法 三。字典的基本操作(增删改查) 1.字典的增加操作:字典序列[key] = 值 注意点:如果存

    2024年01月24日
    浏览(58)
  • 第七章 正则表达式

    目录 1.1. 概念: 1.2. 基本正则表达式 1.2.1. 常见元字符 1.2.2. POSIX字符类 1.2.3. 示例 1.3. 扩展正则表达式 1.3.1. 概念 1.3.2. 示例 在进行程序设计的过程中,用户会不可避免地遇到处理某些文本的情况。有的时候,用户还需要查找符合某些比较复杂规则的字符串。对于这些情况,如

    2024年03月17日
    浏览(87)
  • 第七章 高级 OOP 特性

    7.3.3 继承与延迟静态绑定 在创建类层次结构时,有时候回遇到这种情况,即父类方法要使用静态类属性,但静态类属性可能在子类中被覆盖。这和 self 的使用有关。我们看一个例子,其中 Employee 类和 Executive 类都做了一些修改: 执行代码如下: Watching Football  因为

    2024年02月11日
    浏览(53)
  • C国演义 [第七章]

    力扣链接 给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。 示例 1: 输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7] 输出:3 解释:长度最长的公共子数组是 [3,2,1] 。 示例 2: 输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0] 输出:5 提示: 1 = nums1.length, num

    2024年02月10日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包