今天在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作为便携式线程库。
答案 0 :(得分:5)
调用函数根本无济于事;即使一个函数没有被声明为inline
,它的主体仍然可以被内联(除非是极端的东西,比如将你的isInitialized()
函数放在另一个库中并动态链接它)。
可以想到两个选项:
将initialized
声明为原子标志(在C ++ 0x中,您可以使用std::atomic_flag
;否则,您需要查阅线程库的文档以了解如何操作此)
使用信号量;在另一个线程中获取它并在此线程中等待它。
答案 1 :(得分:4)
@ Karl的评论就是答案。在线程B完成初始化之前,不要在线程A中开始处理。他们这样做的关键是从线程B向线程A发送一个信号,它正在向上和向上发送。运行
你提到没有操作系统,所以我会给你一些Windows-ish psudocode。转码到您选择的操作系统/库。
首先创建一个Windows事件对象。这将用作信号:
HANDLE running = CreateEvent(0, TRUE, FALSE, 0);
然后让线程A启动线程B,将事件传递给它:
DWORD thread_b_id = 0;
HANDLE thread_b = CreateThread(0, 0, ThreadBMain, (void*)handle, 0, &thread_b_id);
现在在线程A中,等到事件发出信号:
DWORD rc = WaitForSingleObject(running, INFINITE);
if( rc == WAIT_OBJECT_0 )
{
// thread B is up & running now...
// MAGIC HAPPENS
}
线程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
}
答案 2 :(得分:3)
同步原语是这个问题的解决方案,而不是在循环中旋转......但是如果你必须在循环中旋转而不能使用信号量,事件等,你可以安全地使用volatile
。它被认为是有害的,因为它会伤害优化器。在这种情况下,这正是你想要做的,不是吗?
答案 3 :(得分:0)
有一个相当于atomic_flag的提升,在boost :: once中称为once_flag。这可能是你想要的。
实际上,如果你想在第一次调用它时构造一些东西,例如延迟加载,并且在多个线程中发生,你可以在第一次调用函数时获得boost :: once。后置条件是它已被初始化,因此不需要任何类型的循环或锁定。
您需要确保的是您的初始化逻辑不会抛出异常。
答案 4 :(得分:0)
使用线程时这是一个众所周知的问题。对象的创建/初始化需要相对较少的时间。当线程实际开始运行时......就执行的代码而言,这可能需要相当长的时间。
每个人都不断提到信号量......
您可能需要查看POSIX 1003.1b信号量。在Linux下,尝试 man sem_init 。 E.g:
这些信号量的优点是,一旦创建/初始化,一个线程可以无限期地阻塞,直到另一个线程发出信号。更重要的是,该信号可以在等待线程开始等待之前发生。 (信号量和条件变量之间的显着差异。)此外,它们可以处理在唤醒之前收到多个信号的情况。