c++ promise和set_value_at_thread_exit的生命周期

8tntrjer  于 2024-01-09  发布在  其他
关注(0)|答案(3)|浏览(109)

假设我们有以下代码:

std::promise<int> promise;
auto future = promise.get_future();

const auto task = [](auto promise) {
    try {            
        promise.set_value_at_thread_exit(int_generator_that_can_throw());
    } catch (...) {
        promise.set_exception_at_thread_exit(std::current_exception());
    }
};
    
std::thread thread(task, std::move(promise));
// use future
thread.join();

字符串
我想知道这个代码是否正确和安全,如果不是,为什么。
用GCC编译时,它似乎工作得很好,但会崩溃(没有消息打印)当使用MSVC(2017)编译时。我的猜测是崩溃发生,因为task内部的promise局部变量超出范围并过早销毁。如果我删除_at_thread_exit后缀,这段代码按预期工作(或看起来工作)。当promise被捕获时,它也能正确工作:

const auto task = [p = std::move(promise)]() mutable {
    /*...*/
};


Complete compilable example

p1iqtdky

p1iqtdky1#

为什么你的代码会产生问题?让我们从answer到“当_at_thread_exit写入std::futurestd::promise的共享状态时?”开始。它发生在所有线程局部变量被销毁之后。你的lambda在线程内被调用,在它的作用域离开之后,promise已经被销毁了。但是当线程调用你的lambda时有一些线程局部变量会发生什么?好吧,写操作将在销毁std::promise对象后进行。实际上,其余的在标准中是未定义的。似乎可以在销毁std::promise后将数据传递到共享状态,但信息并不存在。
最简单的解决方案当然是这样的:

std::promise<int> promise;
auto future = promise.get_future();

const auto task = [](std::promise<int>& promise) {
    try {            
        promise.set_value_at_thread_exit(int_generator_that_can_throw());
    } catch (...) {
        promise.set_exception_at_thread_exit(std::current_exception());
    }
};
    
std::thread thread(task, std::ref(promise));
// use future
thread.join();

字符串

f8rj6qna

f8rj6qna2#

我最近奋进理解future-promise的机制,也遇到了同样的问题。我读了源代码和C++标准,终于弄明白了是怎么回事。问题不在于“promise被析构,所以垃圾内存被访问”,而是它的不一致性。

TL;DR

如果xx_at_thread_exit被调用,你不应该让std::promise在线程退出之前析构(即传递函数的结尾),因为它会尝试设置值和异常。看起来MS-STL做了标准中规定的事情,而libc++/libstdc++没有。

Future-Promise模型

在C++中,promise表示“承诺给予某些结果”的工作者,而future表示“想要未来结果”的收集器。此外,这样的连接是nonce通道,这意味着每个端只能设置/获取结果一次,除非你把一个std::future变成一个std::shared_future,使它能够得到多次。
共享状态有三种情况:

  • 既不设置结果,也不准备; std::promise可以调用set_valueset_exceptionset_value_at_thread_exitset_exception_at_thread_exit一次。
  • 设置结果,但未就绪;这是因为std::promise调用xx_at_thread_exit,所以当传递给线程的函子结束时(以及在所有局部变量被析构之后),它将准备就绪。
  • 这是因为set_valueset_exception,或xx_at_thread_exit当函子完全结束时。

特别地,当promise在ready之前被析构时,共享状态将在dtor中被 * 放弃 *,这意味着将设置异常std::future_error(破碎的promise)。
共享状态就像std::futurestd::promise都持有的std::shared_ptr;无论谁销毁,都会使引用计数为-1,当引用计数为0时,共享状态被删除。

终身关注

所以现在我们知道了,调用xx_at_thread_exit后,结果被存储了。使ready完全处于共享状态(通常的实现会存储一个条件变量,并在其上注册这样的事件),这不会受到std::promise的销毁的干扰。所以我说问题不在于“promise被销毁,所以垃圾内存被访问”。
那为什么MS-STL会终止程序呢?那是因为C标准规定 abandon 不检查是否设置了结果,而是检查是否准备好了。但是set_value_at_thread_exit还没有让它准备好,所以会设置异常std::future_errorbroken_promise)。
然而,一个共享状态应该有一个值或一个异常!MS-STL基本上只是调用set_exception,所以存储的值使它抛出std::future_errorpromise_already_satisfied)。但是它在noexcept dtor中!所以std::terminate被调用,因此程序被中止。
对于libc
/libstdc++,std::promise一旦设置,就会切断与存储结果的连接,这样dtor中就不会设置异常,程序正常运行,这稍微违反了标准,因为它应该同时有值和异常;但这将违反未来承诺模型。因此,除非修订标准,MS-STL的实现更合理,既能满足标准,又能满足抽象模型。

6vl6ewon

6vl6ewon3#

#include <future>

int main()
{
    auto promise = std::make_shared<std::promise<int>>();
    auto future = promise->get_future();

    const auto task = [](const std::shared_ptr<std::promise<int>>& p) 
    {
        try 
        {
            p->set_value_at_thread_exit(42);
        }
        catch (...) 
        {
            p->set_exception_at_thread_exit(std::current_exception());
        }
    };

    std::thread thread(task, promise);
    // use future
    thread.join();

    return 0;
}

字符串

相关问题