4

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

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

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

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

  • volatile-被认为是有害的
  • 调用isInitialized()函数而不是直接使用变量 - 这会保证内存屏障吗?如果函数被声明了inline怎么办?

还有其他选择吗?

编辑:

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

4

5 回答 5

6

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

想到的两个选项:

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

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

于 2010-12-30T17:25:18.810 回答
4

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

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

首先创建一个 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
}
于 2010-12-30T17:46:21.210 回答
3

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

于 2010-12-30T17:30:16.940 回答
0

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

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

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

于 2010-12-30T18:15:11.333 回答
0

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

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

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

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

于 2010-12-30T19:11:10.570 回答