【问题标题】:How can I avoid threading + optimizer == infinite loop? [duplicate]如何避免线程 + 优化器 == 无限循环? [复制]
【发布时间】:2010-12-30 17:20:10
【问题描述】:

在今天的code review 中,我偶然发现了以下代码(为发布而稍作修改):

while (!initialized)
{
  // The thread can start before the constructor has finished initializing the object.
  // Can lead to strange behavior. 
  continue;
}

这是在新线程中运行的前几行代码。在另一个线程中,一旦初始化完成,它会将initialized 设置为true

我知道优化器可以把它变成一个无限循环,但是避免这种情况的最好方法是什么?

  • volatile - considered harmful
  • 调用isInitialized() 函数而不是直接使用变量- 这会保证内存屏障吗?如果函数声明为 inline 会怎样?

还有其他选择吗?

编辑:

应该早点提到这一点,但这是需要在 Windows、Linux、Solaris 等平台上运行的可移植代码。我们主要使用 Boost.Thread 作为可移植线程库。

【问题讨论】:

  • 为什么不只是......在初始化完成之前不启动线程?即,将启动新线程的代码移动到另一个线程的初始化代码的末尾。
  • @Karl 是的,最好的解决方案是完全避免这种情况。不过,我仍然对一般问题的答案感兴趣。
  • 糟糕,这不是一个精确副本。这不是exit_now 标志,因此您希望std::atomic<bool> 至少带有memory_order_acquiremo_relaxed 在这里 not 就足够了,就像 exit_now 标志一样。关闭后才注意到差异。从标题来看,人们可能不会考虑用其他方法来解决初始化问题(除了 spin-wait)。

标签: c++ multithreading thread-safety compiler-optimization


【解决方案1】:

调用函数根本没有帮助;即使一个函数没有声明为inline,它的主体仍然可以被内联(除非有一些极端的情况,比如将你的isInitialized() 函数放在另一个库中并动态链接它)。

想到的两个选项:

  • initialized 声明为原子标志(在 C++0x 中,您可以使用 std::atomic_flag;否则,您需要查阅线程库的文档以了解如何执行此操作)

  • 使用信号量;在另一个线程中获取它并在这个线程中等待它。

【讨论】:

  • 如果函数体在另一个编译单元(.cpp)中,不一定在另一个库中,我认为不能内联(不知道标准中是否有定义,但是在两个 .cpp 文件之间存在依赖关系会很奇怪)
  • @7vies:编译器可以将一些优化和代码生成延迟到链接时间。例如,Visual C++ 会执行此操作(称为链接时间代码生成,或 LTCG)。
  • 对于无限循环部分:Multithreading program stuck in optimized mode but runs normally in -O0 是它的现代版本。 std::atomic<bool>memory_order_relaxed 是最轻量级的方式。但在这种情况下,它是一个 exit_now 标志。这个用例是不同的;我们需要一个获取负载,因为我们大概要读取另一个线程初始化的数据。无论如何,std::atomic_flag 上的可用操作非常有限,比如只有 test-and-set,而不仅仅是纯写,所以效率低于atomic<bool>
【解决方案2】:

@Karl 的评论就是答案。在线程 B 完成初始化之前,不要在线程 A 中开始处理。他们这样做的关键是从线程 B 向线程 A 发送一个信号,表明它已启动并运行。

你没有提到操作系统,所以我会给你一些 Windows 式的伪代码。转码到您选择的操作系统/库。

首先创建一个 Windows 事件对象。这将用作信号:

线程 A:

HANDLE running = CreateEvent(0, TRUE, FALSE, 0);

然后让线程 A 启动线程 B,将事件传递给它:

线程 A:

DWORD thread_b_id = 0;
HANDLE thread_b = CreateThread(0, 0, ThreadBMain, (void*)handle, 0, &thread_b_id);

现在在线程 A 中,等待事件发出信号:

线程 A:

DWORD rc = WaitForSingleObject(running, INFINITE);
if( rc == WAIT_OBJECT_0 )
{
  // thread B is up & running now...
  // MAGIC HAPPENS
}

线程 B 的启动例程进行初始化,然后发出事件信号:

线程 B:

DWORD WINAPI ThreadBMain(void* param)
{
  HANDLE running = (HANDLE)param;
  do_expensive_initialization();
  SetEvent(running); // this will tell Thread A that we're good to go
}

【讨论】:

    【解决方案3】:

    同步原语是解决这个问题的方法,不是循环旋转...但是如果你必须循环旋转并且不能使用信号量、事件等,你可以安全地使用volatile。它被认为是有害的,因为它会伤害优化器。在这种情况下,这正是您想要做的,不是吗?

    【讨论】:

    • 即使使用 volatile,您仍然需要内存屏障,否则 initialized = true 的效果会在对象初始化效果之前变得可见(因为 CPU 可以重新排序内存访问)。出于这个原因,原子变量和其他同步原语通常具有隐式内存屏障。
    【解决方案4】:

    在 boost::once 中有一个等效于 atomic_flag 的 boost_flag。这可能就是你想要的。

    如果你想在第一次被调用时构建某些东西,例如延迟加载,并且发生在多个线程中,你会得到 boost::once 来在第一次调用你的函数时调用它。后置条件是它已被初始化,因此不需要任何类型的循环或锁定。

    您需要确保您的初始化逻辑不会引发异常。

    【讨论】:

      【解决方案5】:

      这是使用线程时众所周知的问题。对象的创建/初始化花费的时间相对较少。但是当线程真正开始运行时……就执行的代码而言,这可能需要相当长的时间。

      每个人都在不断提到信号量......

      您可能想查看 POSIX 1003.1b 信号量。在 Linux 下,尝试 ma​​n sem_init。例如:

      这些信号量的优点是,一旦创建/初始化,一个线程可以无限期地阻塞,直到另一个线程发出信号。更关键的是,该信号可能在等待线程开始等待之前出现。 (信号量条件变量之间的显着区别。)此外,它们可以处理您在醒来之前接收到多个信号的情况。

      【讨论】:

        猜你喜欢
        • 2019-05-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多