The class template std::promise provides a facility to store a value or an exception that is later acquired asynchronously via a std::future object created by the std::promise object. Note that the std::promise object is meant to be used only once. -- cppreference/promise

# 1、概述

std::promise 是 C++11 引入的一种线程间通信机制,用于在一个线程中设置值,然后在另一个线程中获取该值。它通常与 std::future 一起使用, std::promise 设置值,而 std::future 获取值。

其主要用途包括:

  1. 在异步操作中传递结果: 一个线程(生产者)计算一个结果,并将结果设置到 std::promise 中,另一个线程(消费者)通过与 std::promise 关联的 std::future 获取结果。

  2. 实现任务编排: 可以将 std::promise 作为任务的状态指示器,一个任务通过设置 std::promise 的值来通知另一个任务它已经完成。

  3. 异常处理: 可以在一个线程中设置 std::promise 异常,然后在另一个线程中通过与 std::future 关联的 std::future::get() 来捕获异常。

以下是一个简单示例,在函数 addTwoValue 中,我们传入一个 promise 并用它来设置计算结果,在 main 函数中,我们创建一个线程函数用于执行 addTwoValue ,然后获取与 promise 相绑定的 future,并通过该 future 能够获取到 promise 设置的计算结果。(如果计算未完成,promise 还未设置结果,则 future 会阻塞在 get 函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <chrono>
#include <iostream>
#include <thread>
#include <future>


void addTwoValue(std::promise<int> prom, int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // this is invalid on Windows
int result = a + b;
prom.set_value(result);
}



int main() {
std::promise<int> prom;
std::future<int> fu = prom.get_future();

std::thread task(addTwoValue, std::move(prom), 5, 3);
std::cout << "waiting for thread to generate result" << std::endl;

int result = fu.get();
std::cout << "getting result " << result << std::endl;

task.join();

return 0;
}

这里稍微与下面这个示例做辨析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <chrono>
#include <iostream>
#include <thread>
#include <future>


void addTwoValue(std::promise<int>& prom, int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // this is invalid on Windows
int result = a + b;
prom.set_value(result);
}



int main() {
std::promise<int> prom;

std::thread task(addTwoValue, std::ref(prom), 5, 3);
std::cout << "waiting for thread to generate result" << std::endl;

std::future<int> fu = prom.get_future();
int result = fu.get();
std::cout << "getting result " << result << std::endl;

task.join();

return 0;
}

对于 promise 的使用,我们通常用其来设置值,用 future 来获取值,而这个 future,需要是 promise 通过 get_future 接口返回的与该 promise 相绑定的 future。

至于 future 的获取时机,可以在定义 promise 之后就立即获取 future,如上面的示例代码 1,也可以在即将使用时再获取 future,如上面的示例代码 2。

而当我们希望在使用时再获取 future 时,需要注意,对于示例代码 1,由于在 task 传递线程参数的时候使用了移动语义,所以 task 之后的 prom 已经不再有效。所以在示例代码 2 中,我们向 addTwoValue 传递的 promise 是引用,在 task 中我们也需要用 std::ref 进行引用传递。

# 2、promise 在 thread 的传递

为什么在 thread 构造函数传递 promise 时,需要用到 std::move 或者 std::ref 呢?

在 C++ 中,传递 promise 对象给 std::thread 的构造函数时,使用 std::movestd::ref 是为了确保 promise 对象被正确地传递到线程函数中。

# 2.1 使用 std::move

std::move 用于将一个对象从一个作用域转移到另一个作用域,而不进行复制。
这对 promise 对象特别重要,因为 promise 不能被复制,只能被移动

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <thread>
#include <future>

void set_value(std::promise<int> p) {
p.set_value(42);
}

int main() {
std::promise<int> p;
std::future<int> f = p.get_future();

// 使用 std::move 将 promise 转移到线程函数中
std::thread t(set_value, std::move(p));

// 等待线程完成
t.join();

// 获取结果
std::cout << "Value from thread: " << f.get() << std::endl;

return 0;
}

在这个例子中, std::movep 转移到 set_value 函数中,而不是复制它。
如果不使用 std::move ,编译器会报错,因为 std::promise 对象是不可复制的。

# 2.1 使用 std::ref

std::ref 用于创建一个对象的引用包装器,这样对象可以以引用的方式传递,而不是值的方式。
这在某些情况下非常有用,特别是当你不想移动对象,而是希望在多个地方使用同一个对象。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <thread>
#include <future>

void set_value(std::promise<int>& p) {
p.set_value(42);
}

int main() {
std::promise<int> p;
std::future<int> f = p.get_future();

// 使用 std::ref 将 promise 以引用方式传递到线程函数中
std::thread t(set_value, std::ref(p));

// 等待线程完成
t.join();

// 获取结果
std::cout << "Value from thread: " << f.get() << std::endl;

return 0;
}

在这个例子中, std::ref(p) 创建了一个对 p 的引用包装器,传递给线程函数。
这样,线程函数中的 set_value 使用的是主线程中的 promise 对象的引用,而不是一个拷贝或移动的对象。

# 2.3 为什么需要 std::movestd::ref
  1. 不可复制性: std::promise 对象是不可复制的,因此不能通过值传递给 std::thread 的构造函数。必须通过移动语义 std::move 或引用 std::ref 来传递。
  2. 避免资源浪费:移动语义允许资源从一个对象转移到另一个对象,而不是复制所有资源,从而提高了效率。
  3. 线程安全:确保在多线程环境中, promise 对象的状态和结果的设置是安全的。
# 2.4 总结
  • 使用 std::movepromise 对象的所有权转移到线程函数中,这样可以避免复制问题。
  • 使用 std::ref 传递 promise 对象的引用,使得线程函数操作的是原始对象,而不是它的副本。

这两种方法都确保了 promise 对象能够正确地传递给线程函数,并避免了编译错误和资源管理问题。

# 3、promise 的一次性

如何理解 “Note that the std::promise object is meant to be used only once. ” 这句话?

理解 “Note that the std::promise object is meant to be used only once.” 这句话的关键在于理解 std::promise 对象的设计意图和使用场景。 std::promise 对象的核心作用是与 std::future 对象配合使用,在多线程编程中提供一种从一个线程向另一个线程传递结果的机制。

# 3.1 std::promise 的一次性使用性

std::promise 对象的生命周期和与其关联的 std::future 对象密切相关。一个 std::promise 对象只能设置一次值,原因如下:

  1. 状态转换: std::promise 对象内部有一个状态机,用于跟踪其状态(未设置值、已设置值、异常状态等)。一旦 promise 对象设置了值或异常,它的状态就不可再变更。

  2. 数据传递的唯一性:在并发编程中, std::promisestd::future 组合使用的主要目的是将一个线程生成的结果传递给另一个线程。如果允许 std::promise 多次设置值,会导致多次传递,破坏了这种一对一的传递模型。

  3. 同步机制: std::future 对象通过等待 std::promise 对象设置值来同步线程。如果 std::promise 可以多次设置值, std::future 就无法明确何时获取最终结果,从而导致同步机制失效。

# 3.2 总结

std::promise 对象只能使用一次” 这句话意味着:

  • 一个 std::promise 对象只能设置一次值或一次异常。
  • 一旦设置, std::promise 对象的状态就固定了,不能再更改。
  • 这种设计保证了 std::future 对象能明确知道何时可以获取结果,从而实现线程之间的可靠同步。

理解这一点对于正确使用 std::promisestd::future 进行线程间通信非常重要。如果需要传递多个值,可以使用其他同步机制或创建多个 std::promisestd::future 对象来实现。

参考链接:
stackoverflow: what is promise?

更新于

请我喝杯咖啡吧~

Rick 微信支付

微信支付

Rick 支付宝

支付宝