同步线程 - 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_weakcompare_exchange_strong 函数。这些用于将对象的值设置为某个 NewValue,前提是它的当前值为 TestValue 并指示此尝试的状态(是否进行了更改)。这些函数的好处是,这是以这样一种方式完成的,即您可以保证如果两个线程尝试执行此操作,则只有一个线程会成功。这在您需要根据操作结果采取某些操作时非常有用。

【讨论】:

以上是关于同步线程 - InterlockedExchange的主要内容,如果未能解决你的问题,请参考以下文章

线程同步&线程池

线程同步

线程同步之读写锁

深入解析Python中的线程同步方法

同步-同步锁-死锁-线程交互-线程综合示例

多线程同步