Modern Effective C++ 条款三十八:关注不同线程句柄的析构行为

news2024/12/12 18:32:58
之前内容的总结:

item37中说明了可结合的std::thread对应于执行的系统线程。未延迟(non-deferred)任务的future(参见item36)与系统线程有相似的关系。

因此,可以将std::thread对象和future对象都视作系统线程的句柄handles)。

std::threadfuture在析构时有相当不同的行为。item37 说明,可结合的std::thread析构会终止你的程序,因为两个其他的替代选择——隐式join或者隐式detach都是更加糟糕的。

在C++中,std::future和std::promise是用于处理异步操作结果的模板类。它们允许一个线程(调用者)等待另一个线程(被调用者)完成计算并获取其结果。这种机制背后的实现通过共享状态(shared state),是一个独立的对象,既不是std::promise也不是std::future,std::promise和std::future都引用了同一个共享状态,共享状态通常是基于堆的对象,但是标准并未指定其类型、接口和实现。标准库的作者可以通过任何他们喜欢的方式来实现共享状态。 当创建一个std::promise<T>对象时,可以通过调用它的get_future()成员函数来获得一个关联的std::future<T>对象。

std::future<T>对象可以用来等待或检查std::promise<T>是否已经设置了值,并最终获取那个值。

共享状态包含了以下信息:

(1)结果存储:实际的结果数据,即被调用者要传递给调用者的值或异常。

(2)同步原语:用于管理对结果的访问的锁或其他同步机制。

(3)状态标志:指示结果是否已经被设置、是否有错误发生等。

当被调用者完成了它的任务,它会调用std::promise的set_value()或者如果发生了异常则调用set_exception()方法,将结果写入共享状态。此时,任何持有与该std::promise相关联的std::future的线程都可以通过调用get()方法来检索结果,将阻塞直到结果可用。

( 目前觉得类似管道 )

由于std::promise通常是局部变量并且会在被调用者结束时销毁,std::future可能被复制成多个std::shared_future实例,因此结果不能直接存储在这些对象内部。相反,它们都指向堆上的共享状态,这个状态负责保存结果直到所有相关的std::future和std::shared_future都被销毁为止。这样的设计使得即使原始的std::promise和第一个std::future被销毁后,其他std::shared_future实例仍然能够访问结果,只要还有至少一个std::future或std::shared_future存在,共享状态就会保持有效,确保只可移动类型的结果可以被正确处理,因为移动操作不会影响共享状态中的实际数据。

future是通信信道的一端,被调用者通过该信道将结果发送给调用者。被调用者(通常是异步执行)将计算结果写入通信信道中(通常通过std::promise对象),调用者使用future读取结果。

可以想象调用者,被调用者,共享状态之间关系如下图,虚线还是表示信息流方向:

共享状态非常重要,因为future的析构函数,取决于与future关联的共享状态。

在C++中,std::future 与 std::shared_future 类用于获取由异步操作产生的结果或异常。std::future 对象是独占的,而 std::shared_future 可以被多个对象共享。这些 future 对象关联着一个共享状态,这个状态包含未来将要得到的结果或异常。

(1)未延迟任务(即立即执行的任务)

当使用 std::async 启动一个任务时,默认情况下(如果没有指定 std::launch::deferred),任务会立即在一个独立线程中开始执行。返回的 std::future 对象引用了该任务的共享状态。如果这是最后一个引用该共享状态的 std::future 实例(例如,当它离开作用域并被销毁时),那么它的析构函数将会阻塞当前线程直到异步任务完成。这是因为最后一个 std::future 的销毁表示不再有其他地方等待这个异步操作的结果了,所以 C++ 标准库保证任务已经完成,并且结果或异常已经被处理。

(2)其他所有 std::future 的析构

如果有多个 std::future 或 std::shared_future 对象共享同一个状态,它们的销毁不会导致阻塞,除非它是最后一个引用该共享状态的对象。对于立即执行的任务,底层线程的行为类似于调用了 detach ,即使没有 std::future 对象再监视它,它也会继续运行。而对于延迟任务(通过 std::launch::deferred 指定),如果这是最后一个引用共享状态的 std::future 并且它被销毁了,那么该延迟任务永远不会被执行,因为它依赖于至少存在一个 std::future 来触发它的执行。

上面规则的解释:

真正要处理的是一个简单的“正常”行为以及一个单独的例外。正常行为是future析构函数销毁future。就是这样。意味着不join也不detach,也不运行什么,只销毁future的数据成员(当然,还做了另一件事,就是递减了共享状态中的引用计数,这个共享状态是由引用它的future和被调用者的std::promise共同控制的。引用计数见item19)

正常行为的例外情况仅在某个future同时满足下列所有情况下才会出现:

  • 它关联到由于调用std::async而创建出的共享状态
  • 任务的启动策略是std::launch::async(参见item36),原因是运行时系统选择了该策略,或者在对std::async的调用中指定了该策略。
  • 这个future是关联共享状态的最后一个future

对于std::future,情况总是如此,对于std::shared_future,如果还有其他的std::shared_future,与要被销毁的future引用相同的共享状态,则要被销毁的future遵循正常行为(简单地销毁它的数据成员)。只有当上面的三个条件都满足时,future的析构函数才会表现“异常”行为,就是在异步任务执行完之前阻塞住。实际上,这相当于对由于运行std::async创建出任务的线程隐式join

正常行为

对于大多数情况下的 std::future 或 std::shared_future 的析构,行为是相当直接和简单的:

(1)销毁数据成员:当一个 std::future 被销毁时,会简单地清理自己的资源,包括它的内部数据成员。

(2)递减引用计数:更重要的是,它会对共享状态中的引用计数进行递减操作。这个引用计数用于追踪有多少个 std::future 或 std::shared_future 正在引用同一个共享状态。这是管理资源生命周期的一种方式,确保只有当没有其他 future 对象引用该状态时,相关资源才能被安全地释放。

异常行为(特殊条件)

异常行为仅在特定条件下发生,即当以下所有条件同时满足时:

(1)关联到由 std::async 创建的共享状态:意味该 future 是通过调用 std::async 来创建的,而 std::async 通常用于启动异步任务。

(2)任务使用 std::launch::async 策略:这里指的是异步任务确实是在一个独立的线程中执行的。如果策略是 std::launch::deferred,那么任务会在第一次访问结果时才被执行,并且不会立即开始。

(3)最后一个引用此共享状态的 future:如果这是一个与共享状态关联的最后一个 std::future 或 std::shared_future,则意味着不再有其他对象等待这个异步操作的结果。

异常行为的表现

当上述三个条件都满足时,std::future 的析构函数将表现出“异常”行为。它不会简单地销毁自己并递减引用计数,而是会阻塞当前线程,直到关联的异步任务完成。这实际上相当于对启动该任务的线程进行了隐式的 join 操作。这种设计确保了即使没有任何显式代码在等待结果,也能保证异步任务正确完成,防止潜在的资源泄漏或未定义行为。

关于 std::future 析构时的行为不确定性

问题核心:无法通过 std::future 的 API 确定它是否引用了由 std::async 创建的共享状态,因此不能提前知道析构函数是否会因为等待异步任务完成而阻塞。

std::vector<std::future<void>> futs; 
// 可能在析构时阻塞,如果其中至少一个 future 引用了 std::async 启动的任务。

解决方案:当使用 std::packaged_task 创建共享状态时,可以明确知道该 std::future 不会因 std::async 而产生阻塞行为,因为它关联的是 std::packaged_task 创建的共享状态。

int calcValue(); //函数定义
//包裹函数以异步运行
std::packaged_task<int()> pt(calcValue);
auto fut = pt.get_future(); //获取 future

std::thread 和 std::packaged_task 的结合使用

线程管理:当将 std::packaged_task 传递给 std::thread 执行时,需要确保正确地处理线程的生命周期(join 或 detach),这通常会在代码中显式地进行,从而避免 std::future 的析构函数阻塞。

{
    std::packaged_task<int()> pt(calcValue); 
    auto fut = pt.get_future();
    // 将 task 移交给 thread 执行
    std::thread t(std::move(pt));
    // 此处可以对 t 进行 join 或 detach 操作
    // 如果不做任何操作,t 在作用域结束时会导致程序终止
    // 示例:
    // t.join(); // 显式 join
    // 或者
    // t.detach(); // 显式 detach
} // 结束代码块

当有一个与 std::packaged_task 创建的共享状态相关联的 std::future 时,不需要担心其析构函数会执行异常行为,因为通常会在代码中做出关于线程生命周期的决定(例如,调用 join 或 detach)。此外,如果要使用 std::async 来运行任务,就没有必要再额外使用 std::packaged_task,因为 std::async 已经涵盖了 std::packaged_task 的所有功能。

请记住:

  • future的正常析构行为就是销毁future本身的数据成员。
  • 引用了共享状态,使用std::async启动的未延迟任务建立的最后一个future的析构函数会阻塞住,直到任务完成。

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

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

相关文章

【Spring】IoC和DI,控制反转,Bean对象的获取方式

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 一&#xff1a;什么是IoC 1&#xff1a;什么是容器 2&#xff1a;什么是IoC 二&#xff1a;IoC应用…

【网络协议栈】TCP/IP协议栈中重要协议和技术(DNS、ICMP、NAT、代理服务器、以及内网穿透)

每日激励&#xff1a;“请给自己一个鼓励说&#xff1a;Jack我很棒&#xff01;—Jack” 绪论​&#xff1a; 本章是TCP/IP网络协议层的完结篇&#xff0c;本章将主要去补充一些重要的协议和了解一些网络中常见的名词&#xff0c;具体如&#xff1a;DNS、ICMP、NAT、代理服务器…

离屏渲染概述

我们知道&#xff0c;图像的处理基本都是在GPU中进行&#xff0c;然后GPU将渲染的结果放入当前渲染屏幕的帧缓冲区中&#xff0c;视频控制器取出里面的内容&#xff0c;在屏幕上进行显示。那么有没有什么情况&#xff0c;会因为某些限制&#xff0c;GPU无法将全部的渲染结果直接…

探索 Python 应用的分层依赖:解决 Linux 环境中的 libvirt-python 安装问题

探索 Python 应用的分层依赖&#xff1a;解决 Linux 环境中的 libvirt-python 安装问题 背景Python 版本升级 问题描述原因分析与解决方案 Python 应用的分层依赖&#xff1a;安装与部署的视角libvirt-python的分层依赖尝试的解决方案 使用编译好的 .whl 文件"嫁接"整…

vmware vsphere5---部署vCSA(VMware vCenter Server)附带第二阶段安装报错解决方案

声明 因为这份文档我是边做边写的&#xff0c;遇到问题重新装了好几次所以IP会很乱 ESXI主机为192.168.20.10 VCSA为192.168.20.7&#xff0c;后台为192.168.20.7:5480 后期请自行对应&#xff0c;后面的192.168.20.57请对应192.168.20.7&#xff0c;或根据自己的来 第一阶段…

Unity3D下采集camera场景并推送RTMP服务实现毫秒级延迟直播

技术背景 好多开发者&#xff0c;希望我们能够分享下如何实现Unity下的camera场景采集并推送rtmp服务&#xff0c;然后低延迟播放出来。简单来说&#xff0c;在Unity 中实现采集 Camera 场景并推送RTMP的话&#xff0c;先是获取 Camera 场景数据&#xff0c;通过创建 RenderTex…

指令周期流程图

例题一 例题二 例题三

使用C#通过ColorMatrix对象为图像重新着色

此示例产生了一些令人印象深刻的结果&#xff0c;但实际上非常简单。 它使用其他几个示例演示的 ImageAttribute 技术来快速操作图像的颜色。 下面的AdjustColor方法启动图像着色的过程。 // Adjust the images colors. private Image AdjustColor(Image image) {// Make the …

SQL 在线格式化 - 加菲工具

SQL 在线格式化 打开网站 加菲工具 选择“SQL 在线格式化” 或者直接访问 https://www.orcc.online/tools/sql 输入sql&#xff0c;点击上方的格式化按钮即可 输入框得到格式化后的sql结果

AI作图效率高,亲测ToDesk、顺网云、青椒云多款云电脑AIGC实践创作

一、引言 随着人工智能生成内容&#xff08;AIGC&#xff09;的兴起&#xff0c;越来越多的创作者开始探索高效的文字处理和AI绘图方式&#xff0c;而云电脑也正成为AIGC创作中的重要工具。相比于传统的本地硬件&#xff0c;云电脑在AIGC场景中展现出了显著的优势&#xff0c;…

【密码学】SM4算法

一、 SM4算法简介 SM4算法是中国国家密码管理局于2012发布的一种分组密码算法&#xff0c;其官方名称为SMS4&#xff08;SMS4.0&#xff09;&#xff0c;相关标准为GM/T 0002-2012《SM4分组密码算法》。SM4算法的分组长度和密钥长度均为128比特,采用非平衡Feistel结构。采用32…

Proteus(8.15)仿真下载安装过程(附详细安装过程图)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、Proteus是什么&#xff1f; 二、下载链接 三、下安装步骤 1.解压&#xff0c;有键管理员运行 2.点击Next&#xff0c;进行下一步 3.勾选I accept…&#…

【工业机器视觉】基于深度学习的水表盘读数识别(4-训练与预测)

【工业机器视觉】基于深度学习的仪表盘识读(读数识别)&#xff08;3&#xff09;-CSDN博客 训练与预测 Ultralytics YOLO指的是由Ultralytics公司开发的一系列基于YOLO&#xff08;You Only Look Once&#xff09;架构的目标检测算法。YOLO是一种实时目标检测系统&#xff0c;它…

AlphaPose、yolov8Pose、RTMPose进行对比

一、Alphapose 参考&#xff1a; https://blog.csdn.net/m0_45850873/article/details/123939849

MongoDB-ObjectID 生成器

前言 MongoDB中一个非常关键的概念就是 ObjectID&#xff0c;它是 MongoDB 中每个文档的默认唯一标识符。了解 ObjectID 的生成机制不仅有助于开发人员优化数据库性能&#xff0c;还能帮助更好地理解 MongoDB 的设计理念。 什么是 MongoDB ObjectID&#xff1f; 在 MongoDB …

ARM学习(36)静态扫描规则学习以及工具使用

笔者来学习了解一下静态扫描以及其规则,并且亲身是实践一下对arm 架构的代码进行扫描。 1、静态扫描认识 静态扫描:对代码源文件按照一定的规则进行扫描,来发现一些潜在的问题或者风险,因为不涉及代码运行,所以其一般只是发现一些规范或则一些质量问题,当然这些可能存在潜…

从 Zuul 迁移到 Spring Cloud Gateway:一步步实现服务网关的升级

从 Zuul 迁移到 Spring Cloud Gateway&#xff1a;一步步实现服务网关的升级 迁移前的准备工作迁移步骤详解第一步&#xff1a;查看源码第二步&#xff1a;启动类迁移第三步&#xff1a;引入 Gateway 依赖第四步 编写bootstrap.yaml第五步&#xff1a;替换路由配置第六步&#…

centos部署SkyWalking并在springcloud项目中用法举例

文章目录 场景SkyWalking介绍部署部署Storage [单机版Elasticsearch]部署SkyWalking OAP [下载地址](https://skywalking.apache.org/downloads/#SkyWalkingAPM)部署SkyWalking Java Agent springCloud 使用举例 场景 SkyWalking是应用性能监控平台&#xff0c;可用于分布式系统…

如何借助5G网关实现油罐车安全在线监测

油罐车是常见的特种运输车辆&#xff0c;用以运送各种汽油、柴油、原油等油品&#xff0c;运输危险系数大&#xff0c;而且由于油罐车需要经常行驶在城区道路&#xff0c;为城市各个加油站点、企业工厂运输补充所需油料&#xff0c;因此也是危化品运输车辆的重点监测和管控对象…

【总结·反思·汇报·思考02】裸辞后,我的一些感想和感悟。

Hello&#xff0c;大家好&#xff01; 首先&#xff0c;我需要向大家道个歉&#xff0c;对不起&#xff01;因为最近发生了一些事情&#xff0c;博客文章一直没有更新。&#xff08;90度鞠躬道歉&#xff09; 那么&#xff0c;最近到底发生了什么呢&#xff1f;相信大家已经从…