std::promise生命周期与set_value_at_thread_exit用法安全性问题
你提供的代码并不符合C++标准约束,存在未定义行为,不同编译器下表现不一致(GCC可运行、MSVC 2017崩溃)是未定义行为的典型表现。
std::promise的set_value_at_thread_exit、set_exception_at_thread_exit两个方法有明确的生命周期要求:调用该方法的promise对象,必须保持存活直到当前线程完全退出。
这两个方法的语义是:仅登记要设置到共享状态的值/异常,实际的设置操作会推迟到当前线程所有局部变量、线程存储期变量全部销毁后才执行。
你原始代码中的promise是lambda的形参,属于task函数的局部自动变量:
const auto task = [](auto promise) { // promise是函数局部变量 try { promise.set_value_at_thread_exit(int_generator_that_can_throw()); } catch (...) { promise.set_exception_at_thread_exit(std::current_exception()); } };
当task函数执行完try-catch逻辑返回时,这个局部promise变量就会被销毁。后续线程执行set_value_at_thread_exit登记的设置操作时,操作的是已经被销毁的promise对象,触发未定义行为。
MSVC的std::promise实现依赖promise对象存活到设置操作完成,因此直接崩溃;GCC的实现将设置操作绑定到共享状态而非promise对象本身,因此看起来运行正常,但这不代表代码合法。
1. 去掉_at_thread_exit后缀
set_value/set_exception会在调用时立刻完成共享状态的设置,整个操作发生在promise局部变量销毁之前,操作完成后再销毁promise完全符合规则。
2. 改用lambda捕获promise
const auto task = [p = std::move(promise)]() mutable { /*...*/ };
此时promise是lambda对象的成员变量,而非函数形参。lambda对象作为std::thread持有的可调用对象,生命周期覆盖整个线程执行周期,set_value_at_thread_exit执行设置操作时,p仍然存活,因此行为合法。
内容的提问来源于stack exchange,提问作者Evg

