前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Example】C++ 标准库多线程同步及数据共享 (std::future 与 std::promise)

【Example】C++ 标准库多线程同步及数据共享 (std::future 与 std::promise)

原创
作者头像
芯片烤电池
修改于 2022-04-28 02:07:18
修改于 2022-04-28 02:07:18
1.7K00
代码可运行
举报
文章被收录于专栏:C++教程C++教程
运行总次数:0
代码可运行

阅读此文章前,务必读懂:【Example】C++ 标准库 std::thread 与 std::mutex

否则你会像听天书一样懵。(...)

在任何语言的多线程编程当中,必然涉及线程的同步及数据的共享,方式也有很多种。

C++ 标准库当中提供了同步及共享的方案:std::future 与 std::promise 。

头文件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <future>

一、std::future 与 std::promise

先从最基本且最原始的形式看起,std::future 与 std::promise 是互相配合使用的。

【负责访问】std::future 是一个模板类,它提供了可供访问异步执行结果的一种方式。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
【语法】【伪代码】std::future<Type> name(promise.get_future());

【负责存储】std::promise 也是一个模板类,它提供了存储异步执行的值和异常的一种方式。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
【语法】【伪代码】std::promise<Type> name;

先从最简单的代码入手:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <thread>
#include <future>

void PromiseID(std::promise<std::thread::id> &po) {

    try
    {
        po.set_value(std::this_thread::get_id());
    }
    catch (const std::exception &e)
    {
        po.set_exception(std::current_exception());
    }
    return;
}

int main()
{
    std::promise<std::thread::id> p1;
    std::promise<std::thread::id> p2;

    std::future<std::thread::id> f1(p1.get_future());
    std::future<std::thread::id> f2(p2.get_future());

    std::thread t1(&PromiseID, ref(p1));
    std::thread t2(&PromiseID, ref(p2));

    cout << "thread id 1: " << f1.get() << endl;
    cout << "thread id 2: " << f2.get() << endl;

    t1.join();
    t2.join();

    return EXIT_SUCCESS;
}

以上代码和各种在你目前看来无厘头函数展示了 Print 两个线程 ID 的操作。

首先明白,std::future 负责访问,std::promise 负责存储,同时 promise 是 future 的管理者。

进而就可以先讲简单明了的逻辑:

std::future

1,std::future 是由 std::promise 创建的 (std::async 、std::packaged_task 也可创建 future),也是作为它的管理者。

2,std::future 也仅在创建它的 std::promise、std::async 、std::packaged_task 有效时才可用。

3,std::future 可供异步操作创建者用各种方式查询、等待、提取需要共享的值,也可以阻塞当前线程等待到异步线程提供值。

4,std::future 一个实例只能与一个异步线程相关联。多个线程则需要使用 std::shared_future。

5,std::future 的共享状态是由异步操作所使用的、且与其关联的 std::std::promise 所修改。(当然你单线程修改也行,但抬杠又有什么意义)

6,std::future 禁用了拷贝构造,但是可以进行移动(move)操作。

公共成员函数表:

名称

作用

operator=

移动 future 对象,移动!

share()

返回一个可在多个线程中共享的 std::shared_future 对象。

get()

获取值。(类型由模板类型而定)

valid()

检查 future 是否处于被使用状态,也就是它被首次在首次调用 get() 或 share() 前。

wait()

阻塞等待调用它的线程到共享值成功返回。

wait_for()

在规定时间内 阻塞等待调用它的线程到共享值成功返回。

wait_until()

在指定时间节点内 阻塞等待调用它的线程到共享值成功返回。

共享状态:

补充一些与 std::future 相关的枚举类型,参考自Microsoft Docs:

future_errc 枚举 : 为 future_error 类报告的所有错误提供符号名称。

名称

示意

broken_promise

0

与其关联的 std::promise 生命周期提前结束。

future_already_retrieved

1

重复调用 get() 函数。

promise_already_satisfied

2

与其关联的 std::promise 重复 set。

no_state

4

无共享状态。

future_status 枚举:为计时等待函数可返回的原因提供符号名称。

名称

示意

ready

0

就绪

timeout

1

等待超时

deferred

2

延迟执行(与std::async配合使用)

 

std::promise

1,std::promise 负责存储,注意 std::promise 应当只使用一次。

2,std::promise 的统一初始化构造 "(p)" 是被禁用的,同时赋值运算符 "operator=" 作用为移动,std::promise 不可拷贝,但是可以被引用。

【注:此处应额外补充 alloc 构造函数】

3,std::promise 与 std::future 的状态相关联,它负责将共享值存入并给 std::future 访问使用,值类型也有可能是void、异常,当 std::future 端的阻塞函数接收到后,会立即解除阻塞状态。

4,std::promise 在作为使用者的异步线程当中,应当注意共享变量的生命周期、是否被 set 的问题。如果没有共享值没有被 set,而异步线程却结束,future 端会抛出异常。

5,std::promise 的 set 操作函数只能被调用一次。

6,std::promise 的 get_future() 函数只能被调用一次。

7,std::promise<void> 空类型创建是可以的,任何 set 函数不接受任何形式的参数,此操作用于传递通知,通知与其关联的 std::future 端解除阻塞。

公共成员函数表:

名称

作用

operator=

从另一个 std::promise 移动到当前对象。

swap()

交换移动两个 std::promise。

get_future()

获取与它关联的 std::future。

set_value()

设置值,类型由初始化时的模板类型而定。

set_value_at_thread_exit()

设置值,但是到该线程结束时才会发出通知。

set_exception()

设置异常,类型为 exception_ptr。

set_exception_at_thread_exit()

设置异常,但是到该线程结束时才会发出通知。

一个简单的例子:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
using std::cout;
using std::endl;
#include <vector>
using std::vector;
#include <algorithm>

#include <thread>
#include <future>

void GetVectorMaxToPromise(const vector<int> &vec, std::promise<int> &po) {
    try
    {
        auto it = std::max_element(vec.begin(), vec.end());
        po.set_value_at_thread_exit(*it);
    }
    catch (const std::exception&)
    {
        po.set_exception(std::current_exception());
    }
    return;
}

void PrintIntValue(std::future<int> &fu) {
    cout << "Value: " << fu.get() << endl;
    return;
}

int main()
{
    vector<int> vec = { 1, 2, 3, 4, 5 };

    std::promise<int> po;
    std::future<int> fu(po.get_future());

    std::thread t1(&GetVectorMaxToPromise, ref(vec), ref(po));
    std::thread t2(&PrintIntValue, ref(fu));
    
    t1.join();
    t2.join();

    return EXIT_SUCCESS;
}

这个例子是一个线程获取 vector 当中的最大值并给另一个线程去 print。

在这个非常简单的例子当中可以看到通过 promise to future 做到了线程的同步与值的传递,还有异常的处理。

std::shared_future 与 std::packaged_task

std::future 有个非常明显的问题,就是只能和一个 std::promise 成对绑定使用,也就意味着仅限于两个线程之间使用。

那么多个线程是否可以呢,可以!就是 std::shared_future。

std::shared_future

它的语法是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
【语法】【伪代码】std::shared_future<Type> s_fu(pt.get_future());

std::shared_future 也是一个模板类,它的功能定位、函数接口和 std::future 一致,不同的是它允许给多个线程去使用,让多个线程去同步、共享:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
using std::cout;
using std::endl;
#include <vector>
using std::vector;
#include <sstream>
#include <string>
using std::string;
#include <algorithm>

#include <thread>
#include <future>

int GetVectorMax(const vector<int>& vec) {
    return *(std::max_element(vec.begin(), vec.end()));
}

void PrintIntValueOnShared(std::shared_future<int>& s_fu) {
    s_fu.wait();
    std::stringstream ss;
    ss << std::this_thread::get_id() << " Value: " << s_fu.get();
    cout << ss.str() << endl;
    return;
}

int main()
{
    vector<int> vec = { 1, 2, 3, 4, 5 };

    std::packaged_task<int(const vector<int>&)> pt(GetVectorMax);
    std::shared_future<int> s_fu(pt.get_future());

    std::thread t1(&PrintIntValueOnShared, ref(s_fu));
    std::thread t2(&PrintIntValueOnShared, ref(s_fu));
    std::thread t3(&PrintIntValueOnShared, ref(s_fu));

    Sleep(500); // Windows.h

    std::thread(ref(pt), ref(vec)).join();

    t1.join();
    t2.join();
    t3.join();

    return EXIT_SUCCESS;
}

是的,你还看到了另一个奇怪的东西:std::packaged_task。(...)

std::packaged_task

std::packaged_task 的作用是包装一个可调用对象(可能是函数,也可能是lambda)去给异步线程调用,简化 promise to future 的流程。

它的语法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
【语法】【伪代码】【Callback】
std::packaged_task<Type(ArgsType, ...)> name(Callable);

【语法】【伪代码】【Lambda】
std::packaged_task<Type(ArgsType, ...)> pl([](ArgsType, ...) {
       return TypeData;
});

是的,就像 std::function 那样。只不过它是用来给异步线程调用的:

成员函数表:

名称

作用

operator=

移动 std::packaged_task 对象,移动!

valid()

检查可调用对象是否有效。

swap()

交换移动两个 std::packaged_task。

get_future()

返回具有相关联异步状态的 std::future 对象。

operator()

执行该可调用对象。

make_ready_at_thread_exit

执行该可调用对像,但是到该线程结束时才会发出通知。

reset()

重置,并清空之前的值。

将上文例子变种演示一下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
std::packaged_task<int(const vector<int>&)> pl([](const vector<int>& vec) {
    return *(std::max_element(vec.begin(), vec.end()));
});
std::shared_future<int> s_fu(pl.get_future());

if (pl.valid())
{
    std::thread t1(&PrintIntValueOnShared, ref(s_fu));
    std::thread t2(&PrintIntValueOnShared, ref(s_fu));
    std::thread t3(&PrintIntValueOnShared, ref(s_fu));

    Sleep(500); // Windows.h

    std::thread(ref(pl), ref(vec)).join();

    t1.join();
    t2.join();
    t3.join();
}

使用它需要注意的事项:

1,std::packaged_task 不能被拷贝,但是可以被移动,也可以被引用。

2,std::packaged_task 可以默认无参构造,但此时没有任何作用,执行会发生异常,valid() 值为 false。

3,std::packaged_task 的 get_future() 函数只能被调用一次。

4,std::packaged_task 绑定了可调用对象并已经运行,它的共享状态会一直持续到与它关联的 std::future 或最后一个 std::shared_future 结束为止。

5,std::packaged_task 应谨慎操作,它本身的生命周期应持续到所有与它关联的 future 结束后为止。

std::async

std::async 是一个函数模板,作用是异步运行可调用对象,最终将调用结果返回到 std::future 当中。

它的语法是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
【语法】【伪代码】std::async(LaunchEnum, Callable, Args, ...);
or
【语法】【伪代码】std::async(Callable, Args, ...);
std::async的第一个枚举参数

launch 枚举: 展示描述模板函数 async 的可能模式的位掩码类型

名称

示意

async

0

异步调用 主动

deferred

1

延迟调用 被动

这两个枚举代表什么效果呢?请仔细看非常简单的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
using std::cout;
using std::endl;
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <chrono>

#include <thread>
#include <future>

void PrintFiveStr(const string &str) {

    for (size_t i = 0; i < 5; i++)
    {
        cout << str;
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
    return;
};

int main()
{
    vector<std::launch> launchs = {std::launch::async, std::launch::deferred};
    for (auto &launch : launchs)
    {
        std::future<void> add = std::async(launch, ref(PrintFiveStr), "+");
        std::future<void> sub = std::async(launch, ref(PrintFiveStr), "-");
        add.get();
        sub.get();
        cout << endl;
    }


    return EXIT_SUCCESS;
}

三次运行效果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
【第一次】
+--++--+-+
+++++-----

【第二次】
+-+--++--+
+++++-----

【第三次】
+-+-+-+-+-
+++++-----

是的,最直观的就是:

std::launch::async 是在 std::async 初始化所有线程局域对象后执行可调用对象。

std::launch::deferred 是在 std::async 初始化后(期间完成内部std::thread对象创建),不执行可调用对象(内部std::thread也没有被初始化),在 std::async 返回的 std::future 首次调用非定时等待函数后,再去执行。

这就是[异步调用主动]与[延迟调用被动]的区别。

注意的是,如果不传第一个枚举参数,那么,std::async 优先使用哪种 launch 取决于编译器的实现机制。

额外技术细节请参考 C++ Reference:

函数模板 async 异步地运行函数 f (潜在地在可能是线程池一部分的分离线程中),并返回最终将保有该函数调用结果的 std::future 。 1) 表现如同以 policy 为 std::launch::async | std::launch::deferred 调用 (2) 。换言之, f 可能执行于另一线程,或者它可能在查询产生的 std::future 的值时同步运行。 2) 按照特定的执行策略 policy ,以参数 args 调用函数 f :

若设置 async 标志(即 (policy & std::launch::async) != 0 ),则 async 在新的执行线程(初始化所有线程局域对象后)执行可调用对象 f ,如同产出 std::thread(std::forward<F>(f), std::forward<Args>(args)...) ,除了若 f 返回值或抛出异常,则于可通过 async 返回给调用方的 std::future 访问的共享状态存储结果。

若设置 deferred 标志(即 (policy & std::launch::deferred) != 0 ),则 async 以同 std::thread 构造函数的方式转换 f 与 args... ,但不产出新的执行线程。而是进行惰性求值:在 async 所返回的 std::future 上首次调用非定时等待函数,将导致在当前线程(不必是最初调用 std::async 的线程)中,以 args... (作为右值传递)的副本调用 f (亦作为右值)的副本。将结果或异常置于关联到该 future 的共享状态,然后才令它就绪。对同一 std::future 的所有后续访问都会立即返回结果。

若 policy 中设置了 std::launch::async 和 std::launch::deferred 两个标志,则进行异步执行还是惰性求值取决于实现。

【C++ 14 开始】若 policy 中未设置 std::launch::async 或 std::launch::deferred 或任何实现定义策略标志,则行为未定义。

任何情况下,对 std::async 的调用同步于(定义于 std::memory_order )对 f 的调用,且 f 的完成先序于令共享状态就绪。若选择 async 策略,则关联线程的完成同步于首个等待于共享状态上的函数的成功返回,或最后一个释放共享状态的函数的返回,两者的先到来者。

完工!

2022-03-19 凌晨 4:23

AirChip

====================================

芯片烤电池 C++ Example 2022-Spring Season Pass :

【Example】C++ 标准库常用容器全面概述

【Example】C++ 回调函数及 std::function 与 std::bind

【Example】C++ 运算符重载

【Example】C++ 标准库智能指针 unique_ptr 与 shared_ptr

【Example】C++ 接口(抽象类)概念讲解及例子演示

【Example】C++ 虚基类与虚继承 (菱形继承问题)

【Example】C++ Template (模板)概念讲解及编译避坑

【Example】C++ 标准库 std::thread 与 std::mutex

【Example】C++ 标准库多线程同步及数据共享 (std::future 与 std::promise)

【Example】C++ 标准库 std::condition_variable

【Example】C++ 用于编译时封装的 Pimpl 演示 (编译防火墙 Private-IMPL)

【Example】C++ 单例模式 演示代码 (被动模式、兼容VS2022编译)

====================================

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
C/C++开发基础——std::future与async异步编程
std::future是一个类模板,存放了线程入口函数的返回结果,调用std::future对象的get()函数可以拿到返回结果。
Coder-ZZ
2023/12/18
1.1K0
C/C++开发基础——std::future与async异步编程
C++11异步编程(std::async, std::future, std::packaged_task, std::promise)
       std::async是一个函数模板,会启动一个异步任务,最终返回一个std::future对象。在之前我们都是通过thread去创建一个子线程,但是如果我们要得到这个子线程所返回的结果,那么可能就需要用全局变量或者引用的方法来得到结果,这样或多或少都会不太方便,那么async这个函数就可以将得到的结果保存在future中,然后通过future来获取想要得到的结果。async比起thread来说可以对线程的创建又有了更好的控制,比如可以延迟创建。下面先介绍一下std::future, std::packaged_task, std::promise。
Ch_Zaqdt
2020/02/15
16.2K0
C++异步future
  std::future是C++11标准库中的⼀个模板类,它表⽰⼀个异步操作的结果。当我们在多线程编程中使⽤异步任务时,std::future可以帮助我们在需要的时候获取任务的执⾏结果。std::future的⼀个重要特性是能够阻塞当前线程,直到异步操作完成,从⽽确保我们在获取结果时不会遇到未完成的操作。
用户11029129
2025/01/27
940
C++异步future
C++并发编程 - 同步并发操作
线程同步的本质是防止临界区(公共资源)并发操作,即多个线程禁止同时操作临界区。为此,在程序中以某种手段,将多个线程按照先后顺序访问临界区。
开源519
2022/08/30
1.1K0
UNIX(多线程):12---async、future、packaged_task、promise
【线程1中返回值,线程2调用(promise + future 起连接作用),实现两个线程之间数据传递】
用户3479834
2021/02/03
4620
UNIX(多线程):12---async、future、packaged_task、promise
UNIX(多线程):19---Future 类型详解
本文主要介绍 std::future,std::shared_future 以及 std::future_error,另外还会介绍 <future> 头文件中的 std::async,std::future_category 函数以及相关枚举类型。
用户3479834
2021/02/03
6141
来聊聊C++中头疼的线程、并发
在一个应用程序(进程)中同时执行多个小的部分(线程),这就是多线程。多个线程虽然共享一样的数据,但是却执行不同的任务。
AI算法修炼营
2020/05/08
5.1K0
《C++并发编程实战》读书笔记(2):并发操作的同步
如果线程甲需要等待线程乙完成任务,可以使用C++标准库的条件变量来等待事件发生。<condition_variable>中提供了condition_variable和condition_variable_any,前者只能配合mutex使用,而后者可以与任意符合互斥标准的类型使用,会产生额外开销。主要使用成员函数wait、notify_one、notify_all。
C语言与CPP编程
2023/08/10
3990
《C++并发编程实战》读书笔记(2):并发操作的同步
c++11 多线程入门教程(一)
原文链接:https://www.cnblogs.com/DOMLX/p/10945309.html
徐飞机
2019/06/03
9720
跟面试官刚同步异步编程,有她完全够用了
并行化业务逻辑:经常需要频繁的发送,等待,接收其他业务线程的数据,信息交换是常见且高频的行为,这个时候就要开发高效的异步编程了。
用户9831583
2022/06/16
5870
跟面试官刚同步异步编程,有她完全够用了
[Effective Modern C++(11&14)]Chapter 7: The Concurrency API
1. Prefer task-based programming to thread-based 如果希望异步地运行一个函数 基于线程的做法 int doAsyncWork(); std::thread t(doAsyncWork); 基于任务的做法 auto fut = std::async(doAsyncWork); 区别是:基于线程的做法没办法访问函数的返回值,或者当出现异常时,程序会直接崩溃;而基于任务的做法能够访问返回值,并且能够返回异常的结果,保证程序不会崩溃 C++并发概念中线程的三个含
昊楠Hacking
2018/05/26
9280
【#2】介绍第三方库
🔥 JSONCPP 是一个开源的 C++ 库,用于解析和生成 JSON(JavaScript Object Notation)数据。它提供了简单易用的接口,支持 JSON 的序列化和反序列化操作,适用于处理配置文件、网络通信数据等场景。
IsLand1314
2025/03/26
870
【#2】介绍第三方库
C++多线程通信_c++ socket 多线程
参考: https://m.imooc.com/article/289630 C++11 标准库新引入的线程库 https://www.jianshu.com/p/e5a3498ba930
全栈程序员站长
2022/11/08
1.6K0
std future get_waitkey(0)
1.1 关于std::future_status: std::future_status是一个枚举类型,其值有三:
全栈程序员站长
2022/10/04
4130
c++11新特性之线程相关所有知识点
c++11之前你可能使用pthread_xxx来创建线程,繁琐且不易读,c++11引入了std::thread来创建线程,支持对线程join或者detach。直接看代码:
C语言与CPP编程
2020/12/02
6450
UNIX(多线程):18---异步任务提供者(Provider) 介绍(续)
std::packaged_task 包装一个可调用的对象,并且允许异步获取该可调用对象产生的结果,从包装可调用对象意义上来讲,std::packaged_task 与 std::function 类似,只不过 std::packaged_task 将其包装的可调用对象的执行结果传递给一个 std::future 对象(该对象通常在另外一个线程中获取 std::packaged_task 任务的执行结果)。
用户3479834
2021/02/03
3920
C++11多线程shared_future
       对于future的讲解可以去看这篇博客:std::future,那么对于future来说,因为它的get()函数中实现的是移动语义,所以对于future获取的值,只能get一次,那么如果有多个线程需要对其多次取值的话就需要用shared_future了。shared_future可以通过future对象,来将其转换成shared_future的对象,也可以直接通过shared_future来获取future值。
Ch_Zaqdt
2020/02/17
7740
【C++11】 让多线程开发变得简单--异步操作
C++ 11中提供了异步操作相关类和函数,不同的类具备不同的功能,总体来说类主要有:std::future,std::promise,std::package_task,函数主要是std::async。
CPP开发前沿
2021/11/16
7260
C++基础 多线程笔记(二)
程序运行结果依然是主线程和子线程各自输出1000条信息以及将信息保存到txt文件中,和上篇中 “死锁 & adopt_lock” 的结果类似,这里不再展示。
xxpcb
2020/08/04
5410
C++并发低级接口:std::thread和std::promise
相比std::async,std::thread就原始多了。thread一定会创建新线程(而不是像async那样创建的时候可能不会,后面才创建新线程(std::launch::deferred)),并且创建它的线程还必须指定以何种策略等待新线程。
racaljk
2018/08/31
2.3K0
推荐阅读
相关推荐
C/C++开发基础——std::future与async异步编程
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档