读取互锁变量

时间:2009-04-23 01:44:36

标签: c++ windows multithreading locking interlocked

假设:

一个。 WIN32下的C ++。

B中。正确对齐的易失性整数使用InterlockedIncrement()InterlockedDecrement()递增和递减。

__declspec (align(8)) volatile LONG _ServerState = 0;

如果我想简单地阅读_ServerState,我是否需要通过InterlockedXXX函数读取变量?

例如,我见过代码如:

LONG x = InterlockedExchange(&_ServerState, _ServerState);

LONG x = InterlockedCompareExchange(&_ServerState, _ServerState, _ServerState);

目标是简单地读取_ServerState的当前值。

我不能简单地说:

if (_ServerState == some value)
{
// blah blah blah
}

这个问题似乎有些混乱。我理解在Windows中注册大小的读取是原子的,所以我认为InterlockedXXX函数是不必要的。

马特J.


好的,谢谢你的回复。顺便说一句,这是Visual C ++ 2005和2008。

如果确实如此,我应该使用InterlockedXXX函数来读取_ServerState的值,即使只是为了清楚起见,最好的方法是什么?

LONG x = InterlockedExchange(&_ServerState, _ServerState);

这有修改值的副作用,当我真正想做的就是读它。不仅如此,如果存在上下文切换,我可以将标志重置为错误的值,因为_ServerState的值被推入堆栈以准备调用InterlockedExchange()。< / p>

LONG x = InterlockedCompareExchange(&_ServerState, _ServerState, _ServerState);

我从MSDN上看到的一个例子中得到了这个 见http://msdn.microsoft.com/en-us/library/ms686355(VS.85).aspx

我所需要的只是一切:

lock mov eax, [_ServerState]

在任何情况下,我认为很清楚的一点是提供对标志的线程安全访问,而不会产生关键部分的开销。我已经看到LONG通过InterlockedXXX()函数族使用这种方式,因此我的问题。

好的,我们正在考虑解决当前值的这个问题的一个很好的解决方案是:

LONG Cur = InterlockedCompareExchange(&_ServerState, 0, 0);

10 个答案:

答案 0 :(得分:13)

这取决于你的意思“目标是简单地读取_ServerState的当前值”,它取决于你使用的工具集和平台(指定Win32和C ++,但不指定哪个C ++编译器,那个可能很重要。)

如果您只是想读取值,使得该值未被破坏(即,如果某个其他处理器正在将值从0x12345678更改为0x87654321,则您的读取将获得这两个值中的一个而不是0x12344321)然后只需读取将只要变量是:

就可以了
  • 标记为volatile
  • 正确对齐,
  • 使用具有处理器原子处理的字大小的单个指令读取

这些都不是C / C ++标准所承诺的,但Windows和MSVC确实做出了这些保证,我认为大多数针对Win32的编译器都可以。

但是,如果您希望将读取与其他线程的行为同步,则会有一些额外的复杂性。假设您有一个简单的“邮箱”协议:

struct mailbox_struct {
    uint32_t flag;
    uint32_t data;
};
typedef struct mailbox_struct volatile mailbox;


// the global - initialized before wither thread starts

mailbox mbox = { 0, 0 };

//***************************
// Thread A

while (mbox.flag == 0) { 
    /* spin... */ 
}

uint32_t data = mbox.data;

//***************************

//***************************
// Thread B

mbox.data = some_very_important_value;
mbox.flag = 1;

//***************************

思考是线程A将等待mbox.flag旋转以指示mbox.data具有有效的信息。线程B将一些数据写入mailbox.data,然后将mbox.flag设置为1作为mbox.data有效的信号。

在这种情况下,mbox.flag的线程A中的简单读取可能会获得值1,即使后续读取线程A中的mbox.data没有获得线程B写入的值。

这是因为即使编译器不会重新排序线程B写入mbox.data和mbox.flag,处理器和/或缓存也可能。 C / C ++保证编译器将生成代码,使得线程B在写入mbox.flag之前将写入mbox.data,但处理器和缓存可能有不同的想法 - 称为“内存屏障”或“获取和”的特殊处理必须使用release semantics来确保低于线程指令流级别的排序。

我不确定MSVC以外的编译器是否对低于指令级别的订购提出任何要求。但是,MS确实保证MSVC volatile足够--MS指定易失性写入具有释放语义,而易失性读取具有获取语义 - 尽管我不确定这适用于哪个版本的MSVC - 请参阅http://msdn.microsoft.com/en-us/library/12a04hfd.aspx?ppud=4

我还看到过你所描述的代码,它使用Interlocked API对共享位置执行简单的读写操作。我对此事的看法是使用Interlocked API。锁定自由的线程间通信充满了非常难以理解和微妙的陷阱,并试图在一些关键的代码上采取快捷方式可能最终导致一个非常难以诊断的错误对我来说似乎不是一个好主意。此外,使用Interlocked API会对维护代码的任何人尖叫,“这是需要与其他人共享或同步的数据访问 - 谨慎行事!”。

同样,当使用Interlocked API时,您将从图片中获取硬件和编译器的细节 - 平台确保所有这些内容都得到妥善处理 - 不再需要了解......

阅读关于DDJ的Herb Sutter's Effective Concurrency articles(至少对于我来说恰好是失败的),以获取有关此主题的详细信息。

答案 1 :(得分:6)

你的方式很好:

LONG Cur = InterlockedCompareExchange(&_ServerState, 0, 0);

我正在使用类似的解决方案:

LONG Cur = InterlockedExchangeAdd(&_ServerState, 0);

答案 2 :(得分:5)

互锁指令提供原子性处理器间同步。写入和读取都必须同步,所以是的,您应该使用互锁指令来读取线程之间共享且不受锁保护的值。无锁编程(这就是你正在做的事情)是一个非常棘手的领域,所以你可以考虑使用锁。除非这真的你的程序瓶颈之一必须优化吗?

答案 3 :(得分:1)

32位读取操作在某些 32位系统上已经是原子的(英特尔规范称这些操作是原子的,但不能保证在其他x86兼容平台上也是如此)。所以你不应该使用它来进行线程同步。

如果你需要某种标志,你应该考虑使用Event对象和WaitForSingleObject功能。

答案 4 :(得分:1)

对于任何必须重新访问此主题的人,我想添加到Bartosz所解释的内容,如果标准原子不可用,_InterlockedCompareExchange()是标准atomic_load()的良好替代。这是在i86 Win64上以C语言读取my_uint32_t_var的代码。 atomic_load()被列为基准:

 long debug_x64_i = std::atomic_load((const std::_Atomic_long *)&my_uint32_t_var);
00000001401A6955  mov         eax,dword ptr [rbp+30h] 
00000001401A6958  xor         edi,edi 
00000001401A695A  mov         dword ptr [rbp-0Ch],eax 
    debug_x64_i = _InterlockedCompareExchange((long*)&my_uint32_t_var, 0, 0);
00000001401A695D  xor         eax,eax 
00000001401A695F  lock cmpxchg dword ptr [rbp+30h],edi 
00000001401A6964  mov         dword ptr [rbp-0Ch],eax 
    debug_x64_i = _InterlockedOr((long*)&my_uint32_t_var, 0);
00000001401A6967  prefetchw   [rbp+30h] 
00000001401A696B  mov         eax,dword ptr [rbp+30h] 
00000001401A696E  xchg        ax,ax 
00000001401A6970  mov         ecx,eax 
00000001401A6972  lock cmpxchg dword ptr [rbp+30h],ecx 
00000001401A6977  jne         foo+30h (01401A6970h) 
00000001401A6979  mov         dword ptr [rbp-0Ch],eax 

    long release_x64_i = std::atomic_load((const std::_Atomic_long *)&my_uint32_t_var);
00000001401A6955  mov         eax,dword ptr [rbp+30h] 
    release_x64_i = _InterlockedCompareExchange((long*)&my_uint32_t_var, 0, 0);
00000001401A6958  mov         dword ptr [rbp-0Ch],eax 
00000001401A695B  xor         edi,edi 
00000001401A695D  mov         eax,dword ptr [rbp-0Ch] 
00000001401A6960  xor         eax,eax 
00000001401A6962  lock cmpxchg dword ptr [rbp+30h],edi 
00000001401A6967  mov         dword ptr [rbp-0Ch],eax 
    release_x64_i = _InterlockedOr((long*)&my_uint32_t_var, 0);
00000001401A696A  prefetchw   [rbp+30h] 
00000001401A696E  mov         eax,dword ptr [rbp+30h] 
00000001401A6971  mov         ecx,eax 
00000001401A6973  lock cmpxchg dword ptr [rbp+30h],ecx 
00000001401A6978  jne         foo+31h (01401A6971h) 
00000001401A697A  mov         dword ptr [rbp-0Ch],eax

答案 5 :(得分:0)

应该没问题。它是易失性的,所以优化器不应该肆意破坏你,它是一个32位的值,所以它至少应该是近似原子的。一个可能的惊喜是指令管道可以解决这个问题。

另一方面,使用守卫例程的额外成本是多少?

答案 6 :(得分:0)

当前值读数可能不需要任何锁定。

答案 7 :(得分:0)

Interlocked *功能可防止两个不同的处理器访问同一块内存。在单处理器系统中,您将会没问题。如果你有一个双核系统,你在不同核心上的线程都访问这个值,那么你可能会遇到没有Interlocked *就做你认为是原子的问题。

答案 8 :(得分:0)

阅读没问题。只要不在高速缓存行上分割,32位值总是作为整体读取。你的对齐8保证它总是在缓存行中,所以你会没事的。

忘记重新排序的指令和所有无意义的指令。结果总是按顺序退役。否则将是处理器召回!!!

即使对于双CPU机器(即通过最慢的FSB共享),您仍然会很好,因为CPU通过MESI协议保证缓存一致性。您唯一不能保证的是您阅读的价值可能不是最新的。但是, 最新的是什么?如果您没有根据读取的值写回位置,那么在大多数情况下您可能不需要知道这些内容。否则,您已经使用了互锁操作来处理它。

简而言之,通过在读取时使用Interlocked操作可以获得任何好处(除非提醒下一个人谨慎维护您的代码 - 然后再次,该人可能没有资格维护您的代码开始)。

编辑:回应 Adrian McCarthy 留下的评论。

  

您忽视了编译器优化的效果。如果   编译器认为它已经存在于寄存器中的值,那么它就是   重新使用该值而不是从内存中重新读取它。也,   如果是,编译器可以进行指令重新排序以进行优化   认为没有可观察到的副作用。

我没有说从非易失性变量读取是好的。所有问题都在询问是否需要互锁。事实上,有问题的变量明确用volatile声明。或忽略了关键字volatile的效果?

答案 9 :(得分:0)

您最初的理解基本上是正确的。根据Windows在其支持(或永远支持)的所有MP平台上所需的内存模型,只要它们小于机器字的大小,从标记为volatile的自然对齐变量读取就是原子的。与写入相同。您不需要“锁定”前缀。

如果您在不使用互锁的情况下进行读取操作,则需要进行处理器重新排序。这甚至可以在x86上发生,在有限的情况下:从变量读取可以移动到不同变量的写入之上。在几乎所有Windows支持的非x86架构中,如果不使用显式互锁,则会遇到更复杂的重新排序。

还要求如果您使用比较交换循环,则必须将您要比较的变量标记为volatile。这是一个代码示例,用于说明原因:

long g_var = 0;  // not marked 'volatile' -- this is an error

bool foo () {
    long oldValue;
    long newValue;
    long retValue;

    // (1) Capture the original global value
    oldValue = g_var;

    // (2) Compute a new value based on the old value
    newValue = SomeTransformation(oldValue);

    // (3) Store the new value if the global value is equal to old?
    retValue = InterlockedCompareExchange(&g_var,
                                          newValue,
                                          oldValue);

    if (retValue == oldValue) {
        return true;
    }

    return false;
}

如果它不是易失性的,编译器完全有权在任何时候从g_var重新获取oldValue。这种“重新物化”优化在很多情况下都很好,因为当寄存器压力很高时,它可以避免将寄存器溢出到堆栈中。

因此,该功能的步骤(3)将变为:

// (3) Incorrectly store new value regardless of whether the global
//     is equal to old.
retValue = InterlockedCompareExchange(&g_var,
                                      newValue,
                                      g_var);