编辑:
Ben 是正确的(我说他不是,我是个白痴),cpu 可能会重新排序指令并同时在多个管道中执行它们。这意味着 value=1 可能在执行“工作”的管道完成之前设置。在我的辩护中(不是一个十足的白痴?)我从来没有在现实生活中看到过这种情况,我们确实有一个广泛的线程库,我们确实运行了详尽的长期测试,并且这种模式贯穿始终。如果它发生,我会看到它,但我们的测试都没有崩溃或产生错误的答案。但是……本是正确的,这种可能性是存在的。它可能一直在我们的代码中发生,但是重新排序并没有足够早地设置标志,以至于受标志保护的数据的消费者可以在数据完成之前使用数据。我将更改我们的代码以包含障碍,因为不能保证这将继续在野外工作。我相信正确的解决方案是这样的:
读取值的线程:
...
if (value)
{
__sync_synchronize(); // don't pipeline any of the work until after checking value
DoSomething();
}
...
设置值的线程:
...
DoStuff()
__sync_synchronize(); // Don't pipeline "setting value" until after finishing stuff
value = 1; // Stuff Done
...
话虽如此,我发现this 是对障碍的简单解释。
编译器屏障
内存屏障会影响 CPU。编译器障碍会影响编译器。 Volatile 不会阻止编译器重新排序代码。 Here 了解更多信息。
我相信您可以使用此代码来防止 gcc 在编译时重新排列代码:
#define COMPILER_BARRIER() __asm__ __volatile__ ("" ::: "memory")
所以也许这才是真正应该做的?
#define GENERAL_BARRIER() do { COMPILER_BARRIER(); __sync_synchronize(); } while(0)
读取值的线程:
...
if (value)
{
GENERAL_BARRIER(); // don't pipeline any of the work until after checking value
DoSomething();
}
...
设置值的线程:
...
DoStuff()
GENERAL_BARRIER(); // Don't pipeline "setting value" until after finishing stuff
value = 1; // Stuff Done
...
使用 GENERAL_BARRIER() 可以防止 gcc 重新排序代码,也可以防止 cpu 重新排序代码。现在,我想知道 gcc 是否不会在其内置的内存屏障 __sync_synchronize() 上重新排序代码,这会使 COMPILER_BARRIER 的使用变得多余。
X86
正如 Ben 所指出的,不同的架构对于如何在执行管道中重新排列代码有不同的规则。英特尔似乎相当保守。因此,英特尔可能不需要那么多障碍。不过,这不是避开障碍的好理由,因为这可能会改变。
原帖:
我们一直这样做。它非常安全(并非适用于所有情况,但很多情况下)。我们的应用程序在一个巨大的农场中的 1000 台服务器上运行,每台服务器有 16 个实例,并且我们没有竞争条件。您想知道为什么人们使用互斥锁来保护已经原子操作是正确的。在许多情况下,锁定是浪费时间。在大多数架构上读取和写入 32 位整数是原子的。不要尝试使用 32 位位域!
处理器写入重新排序不会影响一个线程读取另一个线程设置的全局值。事实上,使用锁的结果与不使用锁的结果是一样的。如果您赢得比赛并在更改之前检查值......这与赢得比赛锁定值相同,因此在您阅读它时没有其他人可以更改它。功能相同。
volatile 关键字告诉编译器不要将值存储在寄存器中,而是继续引用原始内存位置。除非您正在优化代码,否则这应该无效。我们发现编译器对此非常聪明,并且还没有遇到 volatile 改变任何东西的情况。编译器似乎很擅长提出寄存器优化的候选者。我怀疑 const 关键字可能会鼓励对变量进行寄存器优化。
如果编译器知道最终结果不会不同,它可能会重新排序函数中的代码。我没有看到编译器对全局变量执行此操作,因为编译器不知道更改全局变量的顺序将如何影响立即函数之外的代码。
如果某个函数正在起作用,您可以使用 __attrribute__ 在函数级别控制优化级别。
现在,也就是说,如果您使用该标志作为网关,只允许组中的一个线程执行某些工作,这将不起作用。示例:线程 A 和线程 B 都可以读取标志。线程 A 被调度。线程 B 将标志设置为 1 并开始工作。线程 A 唤醒并将标志设置为 1 并开始工作。哎呀!为了避免锁定并仍然执行类似的操作,您需要研究原子操作,特别是 gcc atomic builtins,例如 __sync_bool_compare_and_swap(value, old, new)。这允许您设置 value = new 如果 value 当前是旧的。在前面的示例中,如果 value = 1,则只有一个线程(A 或 B)可以执行 __sync_bool_compare_and_swap(&value, 1, 2) 并将 value 从 1 更改为 2。丢失的线程将失败。 __sync_bool_compare_and_swap 返回操作成功。
当你使用 atomic builtins 时,有一个“锁”,但它是一个硬件指令,与使用互斥锁相比非常快。
也就是说,当您必须同时更改大量值时,请使用互斥锁。原子操作(截至今天)仅在所有必须以原子方式更改的数据都可以放入连续的 8、16、32、64 或 128 位时才有效。