2025年c++ 条件变量 future(c++ 条件变量 wait_for)

c++ 条件变量 future(c++ 条件变量 wait_for)svg xmlns http www w3 org 2000 svg style display none svg

大家好,我是讯享网,很高兴认识大家。



 <svg xmlns="http://www.w3.org/2000/svg" style="display: none;"> <path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path> </svg> <p></p> 

讯享网

之前说过,内部的处理逻辑和相似,而且和有密不可分的联系。今天,通过对和源码进行解析,了解二者的处理逻辑和关系。

源码均基于 MSVC 实现

参考:

  1. 博主恋恋风辰的个人博客
  2. up主mq白cpp的个人仓库

有两种重载实现:

讯享网

第一种重载需要显式指定启动策略,也就是我们之前说的、和;第二种重载在使用默认策略时会被调用(也就是只传递可调用对象和参数而不传递启动策略),在内部会调用第一种重载并传入一个策略,并将参数全部转发。

我们只需要着重关注第一种重载即可:

  1. 模板参数和函数体外部信息:
    • :可调用对象的类型
    • :可调用对象所需的参数类型
    • :宏,用于标记该函数的返回值不应被忽略
  2. 返回类型:
     

    其实就是返回一个 对象,是对给定的可调用对象 和参数 执行后返回的类型,其实也就是通过 从 和 中推导出的返回类型。

    我们之前在源码解析中说过内部其实是调用函数,函数负责提供参数并调用传入的可调用对象。

    我们可以把 看作是一个对 的结果类型的封装, 是一个工具,可以调用可调用对象并返回其结果。 提供了一种方式来“推导”出这个结果类型。

    这个类型萃取工具通常长这样(简化版):

    讯享网

    上述代码通过 来推导(decltype) (即可调用对象)在给定参数 上执行后的返回类型。换句话说, 的 成员类型就是可调用对象在调用后的返回类型。

    值得注意的是,所有类型在传递前都进行了 处理,也就是将cv和const修饰符去掉,默认按值传递与 的行为一致。

  3. 形参:
     
      
    • : 表示任务的执行策略,可以是 (表示异步执行)或 (表示延迟执行),或者
    • : 可调用对象,通过完美转发机制将其转发给实际的异步任务
    • : 调用该可调用对象时所需的参数,同样通过完美转发机制进行转发
  4. 和:
    • 就算我们在返回类型中说到的,表示可调用对象的返回类型;
    • : 的定义在大多数情况下和 是相同的,类模板 只是为了处理引用类型以及 void 的情况,参见 的实现:
      讯享网

      为什么需要 _Ptype

      在异步任务的实现中, 是用于将结果与 绑定的对象。 的模板参数通常是可调用对象返回值的类型。在 函数中,我们需要创建一个 对象来存储任务的结果,因此我们需要计算出正确的承诺类型()。也就是说,定义 是为了配合后面 的使用,确保任务的结果可以通过 获取。

      • 是任务返回的类型(由 推导出)。
      • 就是这个返回类型的承诺类型。也就是说, 是 的模板参数类型,表示这个任务结果的类型。

      的定义在大多数情况下和 是相同的,都是可调用对象返回值的类型。

  5. :创建一个 对象 ,其类型为 ,表示与异步任务的结果相关联的承诺(promise)。类型我们之前讲过,这里就不在叙述它的作用,关键还在于其存储的数据成员:
     
      

    注意: 和 并不是同一个模板类,是为了提供对 的进一步定制,并不是本身。模板类的私有成员是通过声明的,即

    讯享网

    类模板是对类模板的包装,并增加了一个表示状态的私有成员 。

     

    状态成员用于跟踪 是否已经调用过 成员函数;它默认为 ,在第一次调用 成员函数时被置为 ,如果二次调用,就会抛出异常。

    的构造函数接受的不是类型的对象,而是类型的指针,用来初始化数据成员 。

    讯享网

    这是因为实际上类型只有两个私有成员:指针,以及一个状态成员:

     

    可以简单理解为 是对 的包装,其中的大部分接口实际上是调用 的成员函数(你们可以去的实现源码中查阅,大部分接口其实都是通过调用实现的)。

    所以在解析源码之前,我们必须对有一个清晰的了解:

    讯享网

    这是模板类主要的成员变量(我没有全部列上去,只列了主要的),其中,最为重要的三个变量是:异常指针互斥量条件变量

    其实, 模板类负责管理异步任务的状态,包括结果的存储、异常的处理以及任务完成的通知。它是实现 和 的核心组件之一,通过 和 类模板对其进行封装和管理,提供更高级别的接口和功能。

    在这里插入图片描述
    讯享网

    、_、 之间的包含关系如上述结构所示。

  6. 初始化 对象:
     

    这是一个函数调用,将我们 的参数全部转发给它。

    1. 首先将参数 (可调用对象)和 (传入可调用对象的参数包) 通过 转发给 。
    2. 然后, 创建一个可调用对象(函数适配器)。
    3. 接着,适配器和指定的启动策略被传递给 函数,目的是获取与异步操作相关的状态。
    4. 最终, 返回一个与异步操作相关的状态,并将其传递给 ,这将会返回一个 ,代表一个异步操作的结果。

    函数根据启动模式(_Policy,有三种)来决定创建的异步任务状态对象类型:

    讯享网

    函数返回一个 指针( 可用于初始化,_可用于初始化),该指针指向一个新的 或 对象。这两个类分别对应于异步任务的两种不同执行策略:延迟执行异步执行

    这段代码也很好的说明, 和 的行为是相同的,都会创建新线程异步执行任务,只不过前者会自行判断系统资源来抉择。


    与 都继承自 ,其用于异步执行任务。它们的构造函数都接受一个函数对象,并将其转发给基类 的构造函数。

     

    类型只有一个数据成员 : 类型的对象 ,它用来存储需要执行的异步任务,而它又继承自 。

    讯享网

    在这里插入图片描述

    如上图所示, 与 都继承自 , 中保存了传入给的可调用对象。同时,继承自, 是类中成员的最基本组成对象,基本所有的接口都是通过调用 的函数实现的。

    与 的构造函数如下:

     

    a. _Task_async_state

    有一个数据成员用于从线程池中获取线程,并执行可调用对象:

    讯享网

    的实现使用了微软实现的并行模式库(PPL)。简而言之, 策略并不是单纯的创建线程让任务执行,而是使用了微软的 ,它从线程池中获取线程并执行任务返回包装对象

    是调用 的父类 的成员函数 .

    有三个版本,自然也有三种版本,用于处理可调用对象返回类型的三种情况

     

    表示执行可调用对象,表示将可调用对象的返回值传入给,其他两个函数也是类似的处理过程。

    、、类似。当 函数对象抛出异常时,控制流会跳转到 代码块;用来记录当前捕获的异常; 标识异常处理的结束;因为返回类型为表示不获取返回值,所以这里通过传递一个 1(表示正确执行的状态)。所有的返回值均传入给。

    简而言之,就是把返回引用类型的可调用对象返回值的引用获取地址传递给 ,把返回 void 类型的可调用对象传递一个 1 (表示正确执行的状态)给 。

    、函数来自模板类的父类,通过这两个函数,传递的可调用对象执行结果,以及可能的异常,并将结果或异常存储在 中

    b. _Deferred_async_state

    不会从线程池中获取一个新线程,然后再新线程中执行任务,而是当前线程调用future的get或者wait函数时,在当前线程同步执行。但它同样调用 函数执行存储的可调用对象,它有一个 函数:

    讯享网

    然后通过 调用可调用对象并通过函数、存储可调用对象返回结果或者异常至。

  7. 返回
     

    在前面说了,其实就是可调用对象返回值的类型。

    传给构造函数的参数之一是:,调用上面构造的的成员函数,该函数用于返回类的私有成员变量。

    函数的实现如下:

    讯享网

    其实就是调用的成员函数检查状态(是否有错),然后判断是否提前返回可调用对象的返回值(如果是,代表future的get被调用,抛出异常);最后,返回。

我们首先从一个最简单的示例开始:

 

我们从之前的学习中了解到,就是从中获取可调用对象的返回结果。唯一的问题是: 内部执行了什么流程?首先从的实现开始:

讯享网

类继承自类,类又有一个类型的私有成员,而的接口实现大部分是通过调用 的成员函数实现的。关系如下:

在这里插入图片描述

但你可能发现一个问题,类怎么没有成员函数????其实,函数继承自父类。

 

而类的其实是通过调用的接口实现的,所以说,在和中是非常核心的。

讯享网

的函数通过保护共享数据,然后调用执行可调用对象,直至调用结束。

 

其实就是通过调用来调用,我们在源码中学习过和`_Call_immediate()。

讯享网

在 函数中调用 是为了确保延迟执行()的任务能够在等待前被启动并执行完毕。这样,在调用 时可以正确地等待任务完成。

因为只有才是当调用时才会执行,其他两种启动策略在大部分情况下都是直接执行,通过获得结果。所以我们必须保证在调用函数时,执行策略的任务被执行,而其他两种启动策略早已经执行任务,无需再调用。所以在函数中,有下面一段,判断任务是否以及执行,如果被执行,那么久就不调用,反之调用。

 

讯享网

通过条件变量挂起当前线程,等待可调用对象执行完毕。在等待期间,当前线程释放持有的锁,保证其他线程再次期间可以访问到共享资源,待当前线程被唤醒后,重新持有锁。其主要作用是:

  1. 避免虚假唤醒:
    • 条件变量的 函数在被唤醒后,会重新检查条件(即 是否为 ),确保只有在条件满足时才会继续执行。这防止了由于虚假唤醒导致的错误行为。
  2. 等待 的任务在其它线程执行完毕:
    • 对于 模式的任务,这段代码确保当前线程会等待任务在另一个线程中执行完毕,并接收到任务完成的信号。只有当任务完成并设置 为 后,条件变量才会被通知,从而结束等待。

这样,当调用 函数时,可以保证无论任务是 还是 模式,当前线程都会正确地等待任务的完成信号,然后继续执行。


其实还有两种特化,不过整体大差不差。

 
讯享网

也就是对返回类型为引用和 void 的情况了。其实先前已经聊过很多次了,无非就是内部的返回引用实际按指针操作,返回 void,那么也得给个 1,表示正常运行的状态。类似于前面 的实现。

函数是的成员函数,而没有继承父类:

 

因为有三种特化,所以函数也有三种特化。它们将当前对象的指针通过转移给类型为的局部变量(转移后,原本的对象便失去了所有权)。然后,局部变量调用成员函数,并将结果返回。

注意:局部对象 在函数结束时析构。这意味着当前对象()失去共享状态,并且状态被完全销毁。

函数的实现如下:

讯享网

成员函数检查 future 当前是否关联共享状态,即是否当前关联任务。如果还未关联,或者任务已经执行完(调用了 get()、set()),都会返回

  • 首先,通过判断当前对象是否关联共享状态,如果没,抛出异常。
  • 最后,调用 的成员函数 ,传递 参数,其实就是代表这个成员函数只能调用一次。

的类型是 ,是一个指针类型,它实际会指向自己的子类对象,我们在讲 源码的时候提到了,它必然指向 或者 。

这其实是个多态调用,父类有这个虚函数:

 

子类 对其进行了重写,以 策略或者策略创建的,实际会调用 :

讯享网

没有对其进行重写,直接调用父类虚函数。

就是 ,调用 成员函数确保任务执行完毕。

其实又是回去调用父类的虚函数了。

方法详解

  1. _Get_value()只能调用一次

    如果 为 且 为 (表示结果已经被检索过),则抛出 错误。

  2. 处理异常

    如果在获取结果过程中出现了异常,需要重新抛出该异常

  3. 设置 为

    在获取值之前,设置 为 ,表示结果已经被检索过。这样可以确保 对象不会被重复获取,避免多次调用 时引发错误。

  4. 执行延迟函数

    调用来运行可能的延迟任务。在该函数内部,如果任务已经运行,那么退出,如果没运行,调用函数执行可调用对象。

  5. 等待结果

    使用条件变量挂起当前线程,确保线程同步,即只有当异步任务准备好返回结果时,线程才会继续执行。

  6. 再次检查异常

    线程被唤醒将结果存储至对象中后,再次判断是否发生了异常,需要重新抛出异常

  7. 返回结果

    这部分代码根据 类型的特性决定如何返回结果:

    • 如果 类型是 默认可构造(即 的默认构造函数有效),直接返回 。
    • 否则,返回

    是一个 引入的类型特征,用于检查类型 是否具有默认构造函数。

    是 中持有的结果,而 是存储在 中的实际值。

是通过执行 函数,然后 再执行 , 再执行 ,再执行并通知线程可以醒来, 获取到我们执行任务的返回值的。以 的偏特化为例:

 

小讯
上一篇 2025-06-16 13:08
下一篇 2025-06-06 12:50

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/200244.html