优先考虑基于任务的编程而非基于线程的编程

优先考虑基于任务的编程而非基于线程的编程

”优先考虑基于任务的编程而非基于线程的编程。对比基于线程的编程方式,基于任务的设计为开发者避免了手动线程管理的痛苦(这种调用方式将线程管理的职责转交给C++标准库的开发者),并且自然提供了一种获取异步执行程序的结果(即返回值或者异常)的方式。“
–《Effective Modern C++》Item 35

先来看一下基于任务的编程,在编码方式长什么样,如下代码所示:

#include <future>int doAsyncWork() {// some workreturn 10;
}int main() 
{auto ans = std::async(doAsyncWork);   // async 返回的是一个 future 对象std::cout << ans.get() << std::endl;  // future 对象的 get 方法获取异步执行程序的结果(返回值)
}

如上代码片段所示,基于任务的并发编程方法通过调用 async 函数并指定任务的函数入口(上述 doAsyncWork 函数),就开启了一个异步任务,并且 async 函数会返回一个 future 对象,用以捕获异步执行程序的返回值。

接下来先了解基于任务的并发编程的相关 API 的基本用法,以及在使用上的一些注意事项,然后对比总结基于任务和基于线程的两种并发编程方式。

async 的基本用法

该网站上对于 async 函数的声明如下(基于C++11),不用关心它复杂的函数声明,在使用时只需要清楚以下两点:

  • 有两个重载版本,区别在于下面的原型2比原型1在第一形参位置多了一个任务“启动策略”的参数,下文会介绍这个参数的作用;其他参数表示的含义都相同;对于原型1,第一个形参为一个可调用对象,第二个参数为可变参数。
  • 返回值为一个 future 类型的变量。
// 原型1
template <class Fn, class... Args>  
future<typename result_of<Fn(Args...)>::type>    async (Fn&& fn, Args&&... args);// 原型2
template <class Fn, class... Args> 
future<typename result_of<Fn(Args...)>::type> async (launch policy, Fn&& fn, Args&&... args);

与 thread 显示的创建一个线程来执行任务不同,调用 async 函数(默认调用方式,上述原型1)并不一定会开启一个新的线程。若想确保使用 async 函数会开启一个新的线程执行任务,需要传入参数(原型2)std::launch::async 。 此外,async 还有一种”启动策略“,延迟启动;在 async 中传入第一个参数 std::launch::deferred。对于 async 的默认”启动策略“(即不显示的传入”启动策略“的参数)为std::launch::async | std::launch::deferred。表示 async 会自动在两者之间选择,这取决于系统和库的实现,通常会针对系统中当前的并发可用性进行优化。

按照眼见为实的常理,应该写一段代码测试一下 async 函数,验证它在“某些情况下”不会开启一个新的线程执行任务,可惜暂时没能找到合适的示例,“暂且就这样认为吧”。

使用 async 的注意事项

async 和 thread 的另一个重要区别为,async 没有将创建的线程 detach 的机制,因此在调用 async 的作用域中,会等待 async 执行的异步任务完成之后才会离开其作用域。下面用一个示例说明该区别。

int doAsyncWork()
{std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "start doAsyncWork" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "end doAsyncWork" << std::endl;return 10;
}void case1()
{std::thread t1(doAsyncWork);std::cout << "main thread end" << std::endl;t1.join();
}void case2()
{auto ans = std::async(doAsyncWork);std::cout << "main thread end" << std::endl;
}/*case1 和 case2 的输出一样:main thread endstart doAsyncWorkend doAsyncWork
*/

如上case2所示,使用 async 进行并发编程时,在调用 async 的作用域中,会等待 async 执行的异步任务完成之后才会离开其作用域,等价于case1中在离开作用域前调用 thread 对象的 join 方法(上述第15行代码)。

接下来看看 async 的返回值。async 的返回值是一个 future 类型的变量,用来捕获传递给 async 的函数对象的返回值或者抛出的异常。

在上面关于 async 的示例中,都使用了 auto 自动类型推导来定义一个变量接收 async 的返回值。若不使用一个变量来接收 async 的返回值,async 就变成了同步操作了,即需要等待传递给 async 的函数对象执行完成后,才会继续向下执行。因此在使用 async 进行并发编程中,一定要捕获其返回值。

先看看 future 对象的基本用法,然后再尝试从语言层面理解 future 的设计。

future 的基本用法

在上文说到,”async 的返回值是一个 future 类型的变量,用来捕获传递给 async 的函数对象的返回值或者抛出的异常。“ 因此,站在一个更高的抽象角度来看,future 类型的变量用来捕获 async 中函数对象在运行过程中的某些状态(下文中的 shared state)。

在API的使用上,若函数对象运行结束,使用 future 的 get 成员方法获取函数对象的返回值,如下示例所示:

int doAsyncWork()
{std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "start doAsyncWork" << std::endl;   std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "end doAsyncWork" << std::endl;return 10;
}int main()
{auto ans = std::async(doAsyncWork);std::cout << ans.get() << std::endl;std::cout << "main thread end" << std::endl;// std::cout << ans.get() << std::endl;
}/*
运行输出为:
start doAsyncWork
end doAsyncWork
10
main thread end注释第13行,取消注释16行,运行输出为:
main thread end
start doAsyncWork
end doAsyncWork
10
*/

对于 future 的 get 成员函数,有两点需要注意:

  • 调用 get 函数和 调用 thread 对象的 join 函数类似,会阻塞当前线程,直至 async 指定的函数对象运行完成。
  • get 函数只能调用一次,再次调用会抛出 std::future_error 异常。

若在 async 指定的函数对象中有异常抛出,get 函数可以提供抛出异常的访问,而基于 thread 的方法,若在线程运行中有异常发生且没有捕获,程序会直接终止(通过调用std::terminate)。如下示例所示:

#include <iostream>
#include <thread>
#include <exception>int doAsyncWork() {std::cout << "start doAsyncWork" << std::endl;throw std::runtime_error("running error.");std::cout << "end doAsyncWork" << std::endl;
}int main() 
{std::thread t1(doAsyncWork);t1.join();std::cout << "main thread end" << std::endl;
}/*运行结果:start doAsyncWorkterminate called after throwing an instance of 'std::runtime_error'what():  running error.Aborted
*/

而基于任务 (async) 的方法,因为 std::async 返回的_future_提供了 get 函数(从而可以获取返回值),当线程对象中异常抛出时,get 函数可以提供异常的访问。如下示例所示:

#include <iostream>
#include <thread>
#include <exception>
#include <future>int doAsyncWork() {std::cout << "start doAsyncWork" << std::endl;throw std::runtime_error("running error.");std::cout << "end doAsyncWork" << std::endl;
}int main() 
{auto ans = std::async(doAsyncWork);try {std::cout << ans.get() << std::endl;}catch(const std::exception& e) {std::cout << e.what() << std::endl;} std::cout << "main thread end" << std::endl;
}/*运行结果为:start doAsyncWorkrunning error.main thread end
*/

future 类中还有 wait、wait_for、wait_until 等成员函数,文档中已有很详细的使用介绍,这里就不再赘述。

从语言层面来对比基于任务编程和基于线程的编程

先从语言层面来理解 async 和 future 的设计。下面两段关于 async 和 future 的英文描述,摘录自 cplusplus.com。

The async function temporarily stores in the shared state either the threading handler used or decay copies of fn and args (as a deferred function) without making it ready. Once the execution of fn is completed, the shared state contains the value returned by fn and is made ready.

A future is an object that can retrieve a value from some provider object or function, properly synchronizing this access if in different threads. “Valid” futures are future objects associated to a shared state. The lifetime of the shared state lasts at least until the last object with which it is associated releases it or is destroyed.

基于任务(async)和基于线程(thread)的并发并发编程从编码模式上看,thread 显示的开启一个线程,并需要显示的处理 thread 的负载均衡、资源释放等问题;而 async 以一种隐式的方式开启一个异步任务,”看不到线程“,从编码模式上看,和同步编程的模式类似,auto res = async(fn) 就像一个正常的函数调用,启动一个异步任务,然后获取返回值,对于线程的管理交给标准库来处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/221680.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024年天津仁爱学院高职升本科专业考试报考须知

2024年天津仁爱学院高职升本科专业考试报考须知 一、报名条件 1.报考天津仁爱学院2024年高职升本科各专业的考生&#xff0c;应符合天津市教育招生考试院制定的2024年天津市高职升本科及天津仁爱学院专业考试有关报考条件&#xff0c;须完成2024年天津市高职升本科文化考…

k8s部署nacos

先决条件: 这里的存储使用的是storageClass,所以要预先将storageClass(nfs)部署完成详情参见: k8s-StoargClass的使用-基于nfs-CSDN博客 因为nacos数据存储依赖于mysql.所以要预先部署MySQL,然后再部署nacos 部署mysql使用的pvc [rootmaster /devops/nacos/mysql]$cat mysql…

jmeter判断’响应断言‘两个变量对象是否相等

1、首先需要设置变量&#xff0c;json、正则、csv文件等变量 2、然后在响应断言中 ①JMeter Variable Name to use —— 输入一个变量&#xff0c;变量名即可 ② 模式匹配规则 ——相等 ③测试模式 ——输入引用的变量命${变量名} &#xff08;注意这里是需要添加一个测试模式…

【CANoe】CANoe中使用RS232

文章目录 1、CANoe中自带示例2、示例讲解2.1CANoe自带Port A和Port B通讯2.2CANoe自带Port A和串口助手通讯 1、CANoe中自带示例 我使用的事CANoe12&#xff0c;RS232路径如下&#xff1a; C:\Users\Public\Documents\Vector\CANoe\Sample Configurations 12.0.75\IO_HIL\RS23…

shiro入门demo(一)身份验证

shiro&#xff08;身份&#xff09;认证&#xff0c;简单来说就是登录/退出。搭建springboot项目&#xff0c;引入shiro和单元测试依赖&#xff1a; <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-…

读书笔记-《数据结构与算法》-摘要6[快速排序]

快速排序 核心&#xff1a;快排是一种采用分治思想的排序算法&#xff0c;大致分为三个步骤。 定基准——首先随机选择一个元素最为基准划分区——所有比基准小的元素置于基准左侧&#xff0c;比基准大的元素置于右侧递归调用——递归地调用此切分过程 快排的实现与『归并排…

Axure的安装及界面基本功能介绍

目录 一. Axure概述 二. Axure安装 2.1 安装包下载 2.2 安装步骤 三. Axure功能介绍​ 3.1 工具栏介绍 3.1.1 复制&#xff0c;剪切及粘贴 3.1.2 选择模式和连接 3.1.3 插入形状 3.1.4 点&#xff08;编辑控点&#xff09; 3.1.5 置顶和置底 3.1.6 组合和取消组合 …

【LeetCode刷题笔记(5)】【Python】【盛最多水的容器】【中等】

文章目录 盛最多水的容器算法题描述示例示例 1示例 2 提示题意拆解解决方案&#xff1a;【双指针】运行结果复杂度分析 结束语 盛最多水的容器 盛最多水的容器 算法题描述 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i…

【排序算法】希尔排序

一&#xff1a;基本介绍 插入排序介绍 1.1 插入排序存在问题 我们看简单的插入排序可能存在的问题 数组 arr {2,3,4,5,6,1} 这时需要插入的数 1(最小), 这样的过程是&#xff1a; {2,3,4,5,6,6}{2,3,4,5,5,6}{2,3,4,4,5,6}{2,3,3,4,5,6}{2,2,3,4,5,6}{1,2,3,4,5,6} 结论:…

AI猫图片展示

AI猫展示 文章目录 AI猫展示

【sgAutocomplete】自定义组件:基于elementUI的el-autocomplete组件开发的自动补全下拉框组件(带输入建议的自动补全输入框)

特性&#xff1a; 1、支持本地保存选中过的记录 2、支持动态接口获取匹配下拉框内容 3、可以指定对应的显示label和字段组件key 4、自动生成速记符字段&#xff08;包含声母和全拼两种类型&#xff09;&#xff0c;增强搜索匹配效率 sgAutocomplete源码 <template><!…

计算机网络:物理层(奈氏准则和香农定理,含例题)

带你速通计算机网络期末 文章目录 一、码元和带宽 1、什么是码元 2、数字通信系统数据传输速率的两种表示方法 2.1、码元传输速率 2.2、信息传输速率 3、例题 3.1、例题1 3.2、例题2 4、带宽 二、奈氏准则&#xff08;奈奎斯特定理&#xff09; 1、奈氏准则简介 2、…

如何选择适合的公共 DNS

中国国内一些公共 DNS 服务 腾讯 DNSPod Anycast&#xff1a;上海、天津、广州、香港 DNS 出口&#xff1a;上述四点 TCP 查询&#xff1a;不支持 DoT、DoH&#xff1a;支持。域名 dns.pub 和 doh.pub&#xff0c;同时支持 DoH 和 DoT ECS&#xff1a;不完整支持 https://www.…

【python并发任务的几种方式】

文章目录 1 Process:2 Thread:3 ThreadPoolExecutor:4 各种方式的优缺点&#xff1a;5 线程与进程的结束方式5.1 线程结束的几种方式5.2 进程的结束方式 6 应用场景效率对比 在Python中&#xff0c;有几种方法可以处理并行执行任务。其中&#xff0c;Process、Thread和ThreadPo…

SQL进阶理论篇(三):什么是索引

文章目录 简介索引是万能的吗索引的种类有哪些&#xff1f;普通/唯一/主键/全文索引聚集索引与非聚集索引单一索引与联合索引 总结参考文献 简介 索引在SQL优化中占了很大的比重&#xff0c;甚至可以说&#xff0c;对SQL的优化&#xff0c;其实就是对索引的优化。 但是索引并…

《深入理解 Android ART 虚拟机》笔记

Dex文件格式、指令码 一个Class文件对应一个Java源码文件&#xff0c;而一个Dex文件可对应多个Java源码文件。开发者开发一个Java模块&#xff08;不管是Jar包还是Apk&#xff09;时&#xff1a; 在PC平台上&#xff0c;该模块包含的每一个Java源码文件都会对应生成一个同文件…

Dubbo连接协议配置基础V2.0.0

Dubbo连接所使用的协议 一、dubbo://协议 1.1、使用场景: Dubbo 缺省协议采用单一长连接和 NIO 异步通讯&#xff0c;适合于小数据量大并发的服务调用&#xff0c;以及服务消费者机器数远大于服务提供者机器数的情况。反之&#xff0c;Dubbo 缺省协议不适合传送大数据量的服…

Flink Window中典型的增量聚合函数(ReduceFunction / AggregateFunction)

一、什么是增量聚合函数 在Flink Window中定义了窗口分配器&#xff0c;我们只是知道了数据属于哪个窗口&#xff0c;可以将数据收集起来了&#xff1b;至于收集起来到底要做什么&#xff0c;其实还完全没有头绪&#xff0c;这也就是窗口函数所需要做的事情。所以在窗口分配器…

计算机组成原理-ATT格式vsIntel格式

文章目录 AT&T格式 vs lntel格式 x86汇编语言是lntel格式&#xff0c;还有一种汇编语言格式是AT&T AT&T格式 vs lntel格式 lntel格式中取主存地址内容未指明长度默认为32位&#xff0c;对应下图中第四行右边的指令 百分号 美元符号 小括号 可用于计算机结构体数组…

竞赛保研 python+opencv+机器学习车牌识别

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于机器学习的车牌识别系统 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;4分工作量&#xff1a;4分创新点&#xff1a;3分 该项目较为新颖&#xff0c;适…