26

认为:

A. 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
}

WRT这个主题似乎有些混乱。我知道寄存器大小的读取在 Windows 中是原子的,所以我认为该InterlockedXXX函数是不必要的。

马特·J。


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

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

LONG x = InterlockedExchange(&_ServerState, _ServerState);

这具有修改值的副作用,而我真正想做的就是读取它。不仅如此,如果有上下文切换,我可能会将标志重置为错误的值,因为 的值_ServerState被压入堆栈以准备调用InterlockedExchange().

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);
4

10 回答 10

12

这取决于您所说的“目标是简单地读取 _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 有效的信号。

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

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

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

我还看到像您描述的那样使用互锁 API 对共享位置执行简单读取和写入的代码。我对此事的看法是使用互锁 API。无锁线程间通信充满了非常难以理解和微妙的陷阱,并且试图在可能最终导致非常难以诊断的错误的关键代码位上走捷径对我来说似乎不是一个好主意. 此外,使用联锁 API 会向维护代码的任何人发出尖叫,“这是需要与其他东西共享或同步的数据访问 -小心行事! ”。

此外,在使用 Interlocked API 时,您将硬件和编译器的细节排除在外 - 平台确保所有这些东西都得到正确处理 - 不用再怀疑......

阅读Herb Sutter 的关于 DDJ 的有效并发文章(至少对我而言,目前恰好是关闭的),以获取有关此主题的良好信息。

于 2009-04-23T04:41:50.593 回答
9

你的方法很好:

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

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

LONG Cur = InterlockedExchangeAdd(&_ServerState, 0);
于 2013-11-14T15:54:03.333 回答
5

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

于 2009-04-23T03:19:35.720 回答
1

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

如果您需要某种标志,则应考虑为此目的使用Event对象和函数。WaitForSingleObject

于 2009-07-27T04:36:14.043 回答
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
于 2016-08-08T14:33:55.873 回答
0

应该没事的。它是易变的,所以优化器不应该对你造成伤害,它是一个 32 位的值,所以它至少应该是原子的。一个可能的惊喜是指令流水线是否可以解决这个问题。

另一方面,使用受保护例程的额外成本是多少?

于 2009-04-23T01:50:18.553 回答
0

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

于 2009-04-23T01:51:06.820 回答
0

Interlocked* 功能可防止两个不同的处理器访问同一块内存。在单处理器系统中,您会没事的。如果您有一个双核系统,其中不同内核上的线程都访问此值,那么在没有 Interlocked* 的情况下执行您认为是原子的操作可能会遇到问题。

于 2009-04-23T01:53:14.397 回答
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,如果它不是 volatile 的话。这种“重新实现”优化在许多情况下都很棒,因为它可以避免在寄存器压力很高时将寄存器溢出到堆栈中。

因此,函数的步骤 (3) 将变为:

// (3) Incorrectly store new value regardless of whether the global
//     is equal to old.
retValue = InterlockedCompareExchange(&g_var,
                                      newValue,
                                      g_var);
于 2012-05-10T07:46:03.883 回答
0

读书就好。32 位值总是作为一个整体读取,只要它没有在高速缓存行上分割。你的 align 8 保证它总是在一个缓存行内,所以你会没事的。

忘记指令重新排序和所有这些废话。结果总是按顺序退出。否则将是处理器召回!!!

即使对于双 CPU 机器(即通过最慢的 FSB 共享),您仍然可以,因为 CPU 通过 MESI 协议保证高速缓存的一致性。您唯一不能保证的是您读取的值可能不是绝对最新的。但是,最新的什么?如果您不根据读取的值回写到该位置,那么在大多数情况下您可能不需要知道这一点。否则,您首先会使用互锁操作来处理它。

简而言之,在读取时使用 Interlocked ops 将一无所获(除了可能提醒下一个维护您的代码的人要小心行事 - 再说一次,那个人可能没有资格维护您的代码)。

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

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

我并没有说从非易失性变量中读取是好的。所有的问题都是问是否需要联锁。事实上,有问题的变量显然是用 . 声明的volatile。还是忽略了关键字的效果volatile

于 2011-07-07T01:36:43.843 回答