【问题标题】:std::call_once safe for non atomic variables?std::call_once 对非原子变量安全吗?
【发布时间】:2018-01-25 03:10:37
【问题描述】:

std::call_once 能否正常工作于非原子变量?考虑以下代码

std::once_flag once;
int x;

void init() { x = 10; }

void f() {
  std::call_once(once, init);
  assert(x == 10);
}

int main() {
  std::thread t1(f), t2(f);
  t1.join();
  t2.join();
}

init 的副作用在call_once 返回时是否会被所有线程看到? cppreference 上的文档有点模糊。它只说在所有线程上std::call_once 将在init 完成后返回,但没有提到任何阻止 x=10 在init 返回后重新排序的内容。

有什么想法吗?阐明行为的标准在哪里?

【问题讨论】:

  • 只有使用 same std::once_flagstd::call_once 调用才是安全的。
  • 注意:来自cppreference的std::call_once的简要定义是“即使从多个线程调用,函数也只调用一次”。这很好解释,不是吗?这是一个很好的问题。
  • std::call_once 的第一个参数应该是once 而不是once_flag
  • @YSC 调用一次函数是很容易的部分。棘手的问题是它是否在另一个线程已经运行该函数的情况下充当内存屏障。
  • @CodesInChaos 来自同一页面 OP 链接:“在上述所选函数的执行成功完成之前,组中没有调用返回”。我一生中读过不太清晰的文档。

标签: c++ multithreading c++11


【解决方案1】:

当 call_once 返回时,所有线程都会看到 init 的副作用吗?

来自init 的副作用对所有调用call_once 的线程都是可见的 主动执行不超过一次(调用init),但可以进行多次被动执行。

§ 30.4.6.2-2 - [thread.once.callonce]

不调用其函数的 call_once 执行是被动执行。调用其 func 的 call_once 的执行是主动执行。

§ 30.4.6.2-3 - [thread.once.callonce]

同步:对于任何给定的 once_flag:所有活动的执行都按总顺序发生;主动执行的完成与(6.8.2)按此总顺序的下一个执行同步; 并且返回的执行与所有被动执行的返回同步。

果然如你所愿

【讨论】:

    【解决方案2】:

    原子变量和非原子变量的主要区别在于,从多个线程(除非所有线程都在读取)对非原子变量的访问需要显式同步,以防止访问可能并发。

    有多种方法可以实现这种同步。最常见的技术涉及互斥体。一个线程对互斥锁的解锁与另一个线程随后对该互斥锁的锁定同步。因此,如果第一个线程写入一个变量,而第二个线程读取该变量,则在写入和读取之间存在显式顺序。然后程序会按照您的预期运行:读取必须看到以该顺序写入的最后一个值。如果不使用互斥锁,则对变量的访问可能是并发的,并且会发生未定义的行为。

    原子变量是自同步的:无论如何,尝试访问同一个原子变量的两个线程将在它们之间产生某种顺序。除此之外,与非原子变量相比,它们没有任何特殊的能力,可以被多个线程访问。

    多个线程使用具有相同标志的std::call_once 设置显式同步:每个 线程仅在init 完成后从std::call_once 返回,因此每个线程必须看到x 的新值。

    编译器只允许在不改变程序可观察行为的范围内对写入进行重新排序。一旦您遵守标准,不允许对非原子变量的写入可能与对同一变量的另一个访问同时发生,您在重新排序方面合理化的竞争条件就会消失。

    【讨论】:

    • 感谢您的精彩总结。
    猜你喜欢
    • 2014-05-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多