std :: promise set_value和线程安全

时间:2017-08-11 04:23:33

标签: c++ multithreading c++11 language-lawyer

我对std::promise::set_value()上的线程安全要求感到有些困惑。

standard says

  

效果:以原子方式将值r存储在共享状态并生成   状态准备

但是,它还说promise::set_value()只能用于设置一次值。如果多次调用它,则抛出std::future_error。所以你只能设定一次承诺的价值。

事实上,几乎每个教程,在线代码示例或std::promise的实际用例都涉及 2 线程之间的通信通道,其中一个线程调用std::future::get(),另一个线程调用std::promise::set_value()

我从未见过多个线程可能会调用std::promise::set_value()的用例,即使他们这样做,除了一个之外的所有线程都会导致抛出std::future_error异常。

那么为什么调用std::promise::set_value()的标准命令是原子的呢?同时从多个线程调用std::promise::set_value()的用例是什么?

修改

由于这里的最高投票答案并没有真正回答我的问题,我认为我所要求的不清楚。所以,澄清一下:我知道未来和承诺是什么以及它们如何运作。我的问题是,为什么标准坚持认为std::promise::set_value()必须是原子的?这是一个比“为什么必须在拨打promise::set_value()和拨打future::get()”之间没有比赛的问题更为微妙的问题?

事实上,这里的许多答案(错误地)都会回答原因是因为如果std::promise::set_value()不是原子的,那么std::future::get()可能会导致竞争条件。但是这是错误的。

避免竞争条件的唯一要求是std::promise::set_value()必须与std::future::get()建立happens-before关系 - 换句话说,必须保证std::future::wait()返回时,std::promise::set_value()已完成。

这与std::promise::set_value()本身完全正交是原子的还是非原子的。在使用条件变量的典型实现中,std::future::get()/wait()将等待条件变量。然后,std::promise::set_value()可以非原子地执行任意复杂的计算以设置实际值。然后它将通知共享条件变量(暗示具有释放语义的内存栅栏),std::future::get()将唤醒并安全地读取结果。

因此,std::promise::set_value()本身不需要是原子的来避免竞争条件 - 它只需要满足与std::future::get()发生在之前的关系。

所以再一次,我的问题是:为什么 C ++标准坚持认为std::promise::set_value()必须实际上是一个原子操作,就好像完全在std::promise::set_value()下调用std::promise::set_value()一样互斥锁?除非有多个原因或用例同时调用equals的多个线程,否则我认为没有理由存在此要求。我无法想到这样一个用例,因此这个问题。

3 个答案:

答案 0 :(得分:4)

  

我从未见过多个线程可能调用的用例   std :: promise :: set_value(),即使他们这样做,除了一个之外都会   导致抛出std :: future_error异常。

你错过了承诺和未来的全部想法。

通常,我们有一对承诺和未来。 promise是推送异步结果或异常的对象,未来是异步结果或异常的对象。

在大多数情况下,future和promise对不会驻留在同一个线程上(否则我们会使用一个简单的指针)。所以,您可以将promise传递给某个线程,线程池或某个第三个库异步函数,并从那里设置结果,并将结果拉入调用者线程。

使用std::promise::set_value设置结果的

必须是原子的,不是因为许多promises设置了结果,而是因为驻留在另一个线程上的对象(未来)必须读取结果,并且不执行atomically是未定义的行为,因此设置值并拉动它(通过调用std::future::getstd::future::then)必须以原子方式发生

请记住,每个未来和承诺都有共享状态,设置一个线程的结果更新共享状态,并从共享状态获取结果读取。就像C ++中的每个共享状态/内存一样,当它从多个线程完成时,更新/读取必须在锁定下进行。否则它是未定义的行为。

答案 1 :(得分:4)

如果它不是原子商店,那么两个线程可以同时调用promise::set_value,它执行以下操作:

  1. 检查未来是否准备就绪(即有储值或例外)
  2. 存储值
    • 标记状态就绪
    • 释放阻止共享状态的任何内容
  3. 通过使这个序列成为原子,执行(1)的第一个线程一直到(3),同时调用promise::set_value的任何其他线程将在(1)处失败并引发future_errorpromise_already_satisfied

    没有原子性,两个线程可能存储它们的值,然后一个会成功标记状态就绪,另一个会引发异常,即相同的结果,除了它可能是查看异常的线程中的值。

    在许多情况下,哪个线程可能无关紧要,但是当它确实重要时,如果没有原子性保证,则需要在promise::set_value调用周围包含另一个互斥锁。其他方法(例如比较和交换)不起作用,因为您无法检查未来(除非它{sa shared_future)以确定您的价值是否获胜。

    如果哪个线程赢了并不重要,您可以为每个线程提供自己的未来,并使用std::experimental::when_any来收集可用的第一个结果。< / p>

    经过一些历史研究后编辑:

    虽然上面(使用相同的promise对象的两个线程)看起来并不是一个很好的用例,但当然有一篇当代文章设想将future引入C ++: N2744。本文提出了一些使用案例,这些案例有相互冲突的线程调用set_value,我在这里引用它们:

      

    其次,考虑并行执行两个或多个异步操作的用例,并且&#34;竞争&#34;满足承诺。一些例子包括:

         
        
    • 一系列网络操作(例如请求网页)与定时器上的等待一起执行。
    •   
    • 可以从多个服务器检索值。为了冗余,尝试了所有服务器,但只需要获得第一个值。
    •   
         

    在这两个示例中,要完成的第一个异步操作是满足promise的操作。由于任一操作都可能完成第二次,因此必须编写两者的代码以期望对set_value()的调用失败。

答案 2 :(得分:0)

这些都是很好的答案,但是还有一点很重要。如果没有原子性地设置值,则读取该值可能会受到可观察性的副作用。

例如,在一个简单的实现中:

void thread1()
{
    // do something. Maybe read from disk, or perform computation to populate value
    v = value;
    flag = true;
}

void thread2()
{
    if(flag)
    {
        v2 = v;//Here we have a read problem.
    }
}

std :: promise <>中的原子性允许您避免在一个线程中写入值与在另一个线程中读取值之间的最基本的竞争条件。当然,如果使用std :: atomic <>标志并且使用了正确的fence标志,您将不再有任何副作用,而std :: promise可以保证。