如何保证64位写入是原子的?
在基于Intel x86的平台(特别是使用Intel编译器运行MacOSX 10.4的基于Intel的Mac)上用C编程时,何时可以保证64位写入是原子的?例如:
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位写入将是原子的。这总是真的吗?
解决方案
回答
GCC具有原子操作的内在函数。我怀疑我们也可以对其他编译器执行类似操作。切勿依赖编译器执行原子操作;除非我们明确告诉编译器不要这样做,否则优化几乎肯定会冒着将明显的原子操作变成非原子操作的风险。
回答
在x86_64上,Intel编译器和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页或者附近)。
回答
最好的选择是避免尝试使用基元来构建自己的系统,而应使用锁定,除非它在性能分析时确实显示为热点。 (如果我们认为自己很聪明并且避免使用锁,请不要。我们不是。那是包括我和其他所有人的通用"我们"。)我们至少应使用旋转锁,请参见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上正常工作。
回答
根据Intel处理器手册第3A部分"系统编程指南"的第7章,如果在64位边界上,在Pentium或者更高版本上对齐并且在P6上未对齐(如果仍在缓存行内),则将以原子方式执行四字访问。或者更高版本。我们应该使用" volatile"来确保编译器不会尝试将写操作缓存在一个变量中,并且我们可能需要使用内存隔离例程来确保写操作以正确的顺序进行。
如果需要将值写在现有值的基础上,则应使用操作系统的互锁功能(例如Windows具有InterlockedIncrement64)。
回答
如果我们想为线程间或者进程间通信做类似的事情,那么我们不仅需要原子的读/写保证。在示例中,我们似乎希望写入的值指示某些工作正在进行中和/或者已完成。我们将需要做几件事情,不是全部都是可移植的,以确保编译器按照我们希望它们完成的顺序进行工作(volatile关键字可能在一定程度上有所帮助)并且内存是一致的。现代处理器和高速缓存可能会执行编译器未知的乱序工作,因此我们确实需要某种平台支持(即锁或者特定于平台的互锁API)来完成我们想做的事情。
"内存围栏"或者"内存屏障"是我们可能需要研究的术语。
回答
在Intel MacOSX上,我们可以使用内置的系统原子操作。没有提供32或者64位整数的原子获取或者设置,但是我们可以从提供的CompareAndSwap中构建原子获取或者设置。我们可能希望在XCode文档中搜索各种OSAtomic函数。我已经在下面编写了64位版本。 32位版本可以使用类似命名的函数来完成。
#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函数自动执行该操作:
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位写入的方法。