同步线程 - InterlockedExchange
Posted
技术标签:
【中文标题】同步线程 - InterlockedExchange【英文标题】:Synchronize Threads - InterlockedExchange 【发布时间】:2016-01-22 21:26:51 【问题描述】:我喜欢检查线程是否在工作。如果线程正在工作,我将等待一个事件,直到线程停止工作。线程将在最后设置的事件。
为了检查线程是否在工作,我声明了一个volatile bool
变量。如果线程正在运行,bool
变量将为true
,否则为false
。在线程结束时,布尔变量将被设置为false
。
使用volatile bool
变量是否足够,还是必须使用原子函数?
顺便说一句:请有人解释一下InterlockedExchange
方法,我不明白我需要这个功能的用例。
更新
如果没有我的代码,我还不清楚 volatile bool 变量是否足够。我写了一个测试类来显示我的问题。
class Testclass
public:
Testclass(void);
~Testclass(void);
void doThreadedWork();
void Work();
void StartWork();
void WaitUntilFinish();
private:
HANDLE hHasWork;
HANDLE hAbort;
HANDLE hFinished;
volatile bool m_bWorking;
;
//.cpp
#include "stdafx.h"
#include "Testclass.h"
CRITICAL_SECTION cs;
DWORD WINAPI myThread(LPVOID lpParameter)
Testclass* pTestclass = (Testclass*) lpParameter;
pTestclass->doThreadedWork();
return 0;
Testclass::Testclass(void)
InitializeCriticalSection(&cs);
DWORD myThreadID;
HANDLE myHandle = CreateThread(0, 0, myThread, this, 0, &myThreadID);
m_bWorking = false;
hHasWork = CreateEvent(NULL,TRUE,FALSE,NULL);
hAbort = CreateEvent(NULL,TRUE,FALSE,NULL);
hFinished = CreateEvent(NULL,FALSE,FALSE,NULL);
Testclass::~Testclass(void)
DeleteCriticalSection(&cs);
CloseHandle(hHasWork);
CloseHandle(hAbort);
CloseHandle(hFinished);
void Testclass::Work()
// do some work
m_bWorking = false;
SetEvent(hFinished);
void Testclass::StartWork()
EnterCriticalSection(&cs);
m_bWorking = true;
ResetEvent(hFinished);
SetEvent(hHasWork);
LeaveCriticalSection(&cs);
void Testclass::doThreadedWork()
HANDLE hEvents[2];
hEvents[0] = hHasWork;
hEvents[1] = hAbort;
while(true)
DWORD dwEvent = WaitForMultipleObjects(2, hEvents, FALSE, INFINITE);
if(WAIT_OBJECT_0 == dwEvent)
Work();
else
break;
void Testclass::WaitUntilFinish()
EnterCriticalSection(&cs);
if(!m_bWorking)
// if the thread is not working, do not wait and return
LeaveCriticalSection(&cs);
return;
WaitForSingleObject(hFinished,INFINITE);
LeaveCriticalSection(&cs);
对我来说,m_bWorking 值是原子方式还是 volatile 转换是否足够,还不是很清楚。
【问题讨论】:
InterlockedExchange
不是 C++ 标准函数。
离题:查看message queues 和thread pools。它们可能会在概念上简化您正在尝试做的事情
volatile bool 非常适合这种情况。很容易尝试和查看。
@BitWhistler 您能否再解释一下为什么您认为它完全可以?我不认为是。
“易失性”的意思是它在内存中。对这个变量的每次访问都是从/到内存的,所以当你从一个线程写入它时,其他线程会立即看到它。他们只需要检查。检查一个小型测试程序非常容易,我建议您完成这个实验。事实上,你甚至可以在不使用原子的情况下基于这个事实来创建队列。
【参考方案1】:
您的问题有很多背景知识。例如,我们不知道您使用的是什么工具链,所以我将作为一个 winapi 问题来回答。我进一步假设您有这样的想法:
volatile bool flag = false;
DWORD WINAPI WorkFn(void*)
flag = true;
// work here
....
// done.
flag = false;
return 0;
int main()
HANDLE th = CreateThread(...., &WorkFn, NULL, ..);
// wait for start of work.
while (!flag)
// ?? # 1
// Seems thread is busy now. Time to wait for it to finish.
while (flag)
// ?? # 2
这里有很多问题。对于初学者来说,volatile
在这里做的很少。当flag = true
发生时,它最终将对另一个线程可见,因为它由全局变量支持。之所以如此,是因为它至少会进入缓存,并且缓存有办法告诉其他处理器给定的行(这是一个地址范围)是脏的。它不会进入缓存的唯一方法是,如果编译器进行了超级疯狂的优化,其中flag
作为寄存器留在 cpu 中。这实际上可能会发生,但不会在这个特定的代码示例中发生。
因此 volatile 告诉编译器永远不要将变量保留为寄存器。就是这样,每次看到 volatile 变量时,您都可以将其翻译为“从不注册此变量”。它在这里的使用基本上只是一个偏执的举动。
如果这个代码是你想到的,那么这个标志模式的循环被称为Spinlock
,这个是一个非常糟糕的代码。在用户模式程序中几乎从来都不是正确的做法。
在我们采用更好的方法之前,让我先解决您的 Interlocked 问题。人们通常的意思是这种模式
volatile long flag = 0;
DWORD WINAPI WorkFn(void*)
InterlockedExchange(&flag, 1);
....
int main()
...
while (InterlockedCompareExchange(&flag, 1, 1) = 0L)
YieldProcessor();
...
假设...
表示与以前类似的代码。 InterlockedExchange()
正在做的是强制写入内存以一种确定性的“现在广播更改”的方式发生,并且以相同的“绕过缓存”方式读取它的典型方式是通过 InterlockedCompareExchange()
.
它们的一个问题是它们会在系统总线上产生更多流量。也就是说,总线现在用于在系统上的 cpu 之间广播缓存同步数据包。
std::atomic<bool> flag
将是现代的 C++11 方式来做同样的事情,但仍然不是你真正想做的事情。
我在那里添加了YieldProcessor()
调用以指出真正的问题。当您等待内存地址更改时,您正在使用在其他地方更好地使用的 cpu 资源,例如在实际工作中 (!!)。如果你真的让处理器,操作系统至少有机会将它交给WorkFn
,但在多核机器中它会很快回到轮询变量。在现代机器中,您将每秒检查此flag
数百万次,产量可能每秒 200000 次。无论哪种方式都是可怕的浪费。
您在这里要做的是利用 Windows 进行零成本等待,或者至少是您想要的低成本:
DWORD WINAPI WorkFn(void*)
// work here
....
return 0;
int main()
HANDLE th = CreateThread(...., &WorkFn, NULL, ..);
WaitForSingleObject(th, INFINITE);
// work is done!
CloseHandle(th);
当您从工作线程返回时,线程句柄会收到信号并等待它满足。当卡在WaitForSingleObject
时,您不会消耗任何 CPU 周期。如果您想在等待时在 main() 函数中执行周期性活动,可以将 INFINITE
替换为 1000,这将每秒释放一次主线程。在这种情况下,您需要检查 WaitForSingleObject
的返回值,以告知线程正在完成的超时情况。
如果您需要真正知道工作何时开始,您需要一个额外的可等待对象,例如,通过CreateEvent()
获得的Windows 事件,可以使用相同的WaitForSingleObject
进行等待。
更新 [1/23/2016]
现在我们可以看到您想到的代码,您不需要原子,volatile
就可以了。对于true
情况,m_bWorking
无论如何都受到 cs 互斥锁的保护。
如果我可以建议,您可以使用 TryEnterCriticalSection
和 cs 来完成同样的操作,而无需 m_bWorking
:
void Testclass::Work()
EnterCriticalSection(&cs);
// do some work
LeaveCriticalSection(&cs);
SetEvent(hFinished); // could be removed as well
void Testclass::StartWork()
ResetEvent(hFinished); // could be removed.
SetEvent(hHasWork);
void Testclass::WaitUntilFinish()
if (TryEnterCriticalSection(&cs))
// Not busy now.
LeaveCriticalSection(&cs);
return;
else
// busy doing work. If we use EnterCriticalSection(&cs)
// here we can even eliminate hFinished from the code.
...
【讨论】:
感谢您的回答,但很遗憾这不是正确的。 --> StartWork 和线程真正启动之间的时间窗口不同步。如果 WaitUntilFinish 将在 StartWork 之后但在线程进入临界区之前调用,则 TryEnterCriticalSection 将返回 true,但已经有工作要做。在 StartWork 中输入 CriticalSection 并将其留在 Work 中也将不起作用,因为我无法在不同的线程中输入和离开临界区。 我很抱歉,但在 StartWork 和实际工作之间线程不是“做工作”恕我直言。如果您想要您现在提到的内容,只需跟踪 hHasWork 的状态,使用 WaitforSingleObject(hHasWork, 0) 来获取它的状态。更简单。【参考方案2】:由于某种原因,Interlocked API 不包含“InterlockedGet”或“InterlockedSet”函数。这是一个奇怪的遗漏,典型的解决方法是通过 volatile 进行转换。
您可以在 Windows 上使用如下代码:
#include <intrin.h>
__inline int InterlockedIncrement(int *j)
// This is VS-specific
return _InterlockedIncrement((volatile LONG *) j);
__inline int InterlockedDecrement(int *j)
// This is VS-specific
return _InterlockedDecrement((volatile LONG *) j);
__inline static void InterlockedSet(int *val, int newval)
*((volatile int *)val) = newval;
__inline static int InterlockedGet(int *val)
return *((volatile int *)val);
是的,它很丑。但如果您不使用 C++11,这是解决缺陷的最佳方法。如果您使用的是 C++11,请改用 std::atomic
。
请注意,这是特定于 Windows 的代码,不应在其他平台上使用。
【讨论】:
【参考方案3】:不,volatile bool 是不够的。正如您正确怀疑的那样,您需要一个原子布尔值。否则,您可能永远不会看到您的布尔值更新。
C++ 中也没有 InterlockedExchange
(你的问题的标签),但 C++11 中有 compare_exchange_weak
和 compare_exchange_strong
函数。这些用于将对象的值设置为某个 NewValue,前提是它的当前值为 TestValue 并指示此尝试的状态(是否进行了更改)。这些函数的好处是,这是以这样一种方式完成的,即您可以保证如果两个线程尝试执行此操作,则只有一个线程会成功。这在您需要根据操作结果采取某些操作时非常有用。
【讨论】:
以上是关于同步线程 - InterlockedExchange的主要内容,如果未能解决你的问题,请参考以下文章