关于C#:如何确保64位写入是原子的?

关于C#:如何确保64位写入是原子的?

How to guarantee 64-bit writes are atomic?

在基于Intel x86的平台(特别是使用Intel编译器运行MacOSX 10.4的基于Intel的Mac)上用C编程时,何时可以保证64位写入是原子的? 例如:

1
2
3
4
unsigned long long int y;
y = 0xfedcba87654321ULL;
/* ... a bunch of other time-consuming stuff happens... */
y = 0x12345678abcdefULL;

如果在执行完对y的第一个赋值后另一个线程正在检查y的值,我想确保它看到的是值0xfedcba87654321或值0x12345678abcdef,而不是它们的某种混合。 我想在没有任何锁定的情况下执行此操作,并且如果可能的话,没有任何额外的代码。 我希望在使用支持64位代码(MacOSX 10.4)的操作系统上使用64位编译器(64位Intel编译器)时,这些64位写入将是原子的。 这总是真的吗?


最好的选择是避免尝试使用基元构建您自己的系统,而是使用锁定,除非在分析时它确实显示为热点。 (如果您认为自己很聪明并且避免使用锁,请不要。您不是。那是包括我和其他所有人的通用"您"。)您至少应使用旋转锁,请参见spinlock(3)。无论您做什么,都不要尝试实现"自己的"锁。你会弄错的。

最终,您需要使用操作系统提供的任何锁定或原子操作。在任何情况下,要使这些事情完全正确都是非常困难的。通常,它可能涉及诸如特定处理器特定版本的勘误之类的知识。 ("哦,该处理器的2.0版没有在正确的时间进行缓存一致性侦听,它在2.0.1版中已得到修复,但是在2.0版上,您需要插入NOP。")只是拍拍volatile C中的变量上的关键字几乎总是不足。

在Mac OS X上,这意味着您需要使用atomic(3)中列出的功能来对32位,64位和指针大小的量执行真正的跨所有CPU的操作。 (将后者用于指针上的任何原子操作,因此您将自动与32/64位兼容。)这是否是您要执行原子比较和交换,增量/减量,自旋锁定或堆栈/队列之类的事情的原因。管理。幸运的是,spinlock(3),atomic(3)和barrier(3)函数应该都能在Mac OS X支持的所有CPU上正常工作。


在x86_64上,英特尔编译器和gcc均支持某些固有的原子操作功能。这是他们的gcc文档:http://gcc.gnu.org/onlinedocs/gcc-4.1.0/gcc/Atomic-Builtins.html

英特尔编译器文档也在这里讨论它们:http://softwarecommunity.intel.com/isn/downloads/softwareproducts/pdfs/347603.pdf(第164页或附近)。


根据Intel处理器手册第3A部分-系统编程指南的第7章,如果在64位边界上,在Pentium或更高版本上对齐并且在内存上未对齐(如果仍在高速缓存行内),则将以原子方式执行四字访问。 P6或更高版本。您应该使用volatile来确保编译器不会尝试将写入缓存在变量中,并且您可能需要使用内存隔离例程来确保写入以正确的顺序进行。

如果需要将值写在现有值的基础上,则应使用操作系统的互锁功能(例如Windows具有InterlockedIncrement64)。


在Intel MacOSX上,您可以使用内置的系统原子操作。没有提供32位或64位整数的原子获取或设置,但是您可以从提供的CompareAndSwap中构建原子获取或设置。您可能希望在XCode文档中搜索各种OSAtomic函数。我已经在下面编写了64位版本。 32位版本可以使用类似命名的函数来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <libkern/OSAtomic.h>
// bool OSAtomicCompareAndSwap64Barrier(int64_t oldValue, int64_t newValue, int64_t *theValue);

void AtomicSet(uint64_t *target, uint64_t new_value)
{
    while (true)
    {
        uint64_t old_value = *target;
        if (OSAtomicCompareAndSwap64Barrier(old_value, new_value, target)) return;
    }
}

uint64_t AtomicGet(uint64_t *target)
{
    while (true)
    {
        int64 value = *target;
        if (OSAtomicCompareAndSwap64Barrier(value, value, target)) return value;
    }
}

请注意,Apple的OSAtomicCompareAndSwap函数自动执行该操作:

1
2
3
if (*theValue != oldValue) return false;
*theValue = newValue;
return true;

我们在上面的示例中使用此方法来创建Set方法,方法是先获取旧值,然后尝试交换目标内存的值。如果交换成功,则表明在交换时内存的值仍然是旧值,并且在交换过程中为它提供了新值(它本身是原子的),因此我们完成了。如果它不成功,则说明其他一些线程在获取它和尝试重置它之间修改了它们之间的值,从而产生了干扰。如果发生这种情况,我们可以循环播放并以最小的代价重试。

Get方法背后的思想是,我们可以首先获取该值(如果另一个线程正在干扰,则它可以是或可以不是实际值)。然后,我们可以尝试与自身交换值,只需检查初始抓取是否等于原子值即可。

我尚未针对我的编译器进行检查,因此请原谅任何错别字。

您是专门提到OSX的,但是如果需要在其他平台上工作,则Windows具有许多Interlocked *功能,您可以在MSDN文档中搜索它们。其中一些可以在Windows 2000 Pro及更高版本上运行,而某些(尤其是某些64位功能)是Vista的新增功能。在其他平台上,GCC 4.1和更高版本具有各种__sync *函数,例如__sync_fetch_and_add()。对于其他系统,可能需要使用汇编,并且可以在src / system / libroot / os / arch中的HaikuOS项目的SVN浏览器中找到一些实现。


在X86上,原子写入对齐的64位值的最快方法是使用FISTP。对于未对齐的值,您需要使用CAS2(_InterlockedExchange64)。由于BUSLOCK,CAS2操作非常慢,因此通常可以更快地检查对齐并为对齐的地址执行FISTP版本。确实,这就是英特尔线程构建模块如何实现Atomic 64位写入的方法。


ISO C(C11)的最新版本定义了一组原子操作,包括atomic_store(_explicit)。参见例如此页面以获取更多信息。

原子的第二轻便的实现是GCC内部函数,已经提到过。我发现它们得到了GCC,Clang,Intel和IBM编译器的完全支持,并且-截至我上次检查时-Cray编译器部分地支持了它们。

除了整个ISO标准外,C11原子的一个明显优势是它们支持更精确的内存一致性规定。据我所知,GCC原??子暗示了完整的内存屏障。


如果您想为线程间或进程间通信做类似的事情,那么您不仅需要原子的读/写保证。在您的示例中,您似乎希望写入的值指示某些工作正在进行中和/或已完成。您将需要做一些事情,不是全部都是可移植的,以确保编译器按照您希望它们完成的顺序进行了工作(volatile关键字可能在一定程度上有所帮助)并且内存是一致的。现代处理器和缓存可能会执行编译器未知的乱序工作,因此您确实需要某种平台支持(即锁或特定于平台的互锁API)来完成您想做的事情。

"内存围栏"或"内存屏障"是您可能需要研究的术语。


GCC具有原子操作的内在函数。我怀疑您也可以对其他编译器执行类似操作。切勿依赖编译器执行原子操作;除非您明确告诉编译器不要这样做,否则优化几乎肯定会冒着将明显的原子操作变成非原子操作的风险。


推荐阅读