如何保证64位写入是原子的?

时间:2008-09-16 23:16:56

标签: c multithreading macos atomic lock-free

当在基于Intel x86的平台(特别是使用英特尔编译器运行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位写入将是原子的。这总是如此吗?

8 个答案:

答案 0 :(得分:41)

最好的办法是避免尝试使用原语构建自己的系统,而是使用锁定,除非真正在分析时显示为热点。 (如果你认为你可以聪明并且避免锁定,那就不要。你不是。那是包括我和其他人在内的一般“你”。)你至少应该使用自旋锁,见spinlock(3) 。无论你做什么,尝试实施“你自己的”锁。你会弄错的。

最终,您需要使用操作系统提供的任何锁定或原子操作。在所有情况中获得完全正确的这类内容非常困难。通常它可能涉及特定处理器的特定版本的勘误表之类的知识。 (“哦,该处理器的2.0版本没有在正确的时间执行缓存一致性窥探,它在版本2.0.1中已修复,但在2.0版本中您需要插入NOP。”)只需打一个{ C中变量的{1}}关键字几乎总是不足。

在Mac OS X上,这意味着您需要使用atomic(3)中列出的函数对32位,64位和指针大小的数量执行真正的原子跨所有CPU操作。 (使用后者对指针进行任何原子操作,这样你就可以自动进行32/64位兼容。)无论你是想做原子比较和交换,递增/递减,自旋锁定还是堆栈/队列,都会这样做。管理。幸运的是,spinlock(3)atomic(3)barrier(3)函数都可以在Mac OS X支持的所有CPU上正常运行。

答案 1 :(得分:13)

在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页左右)。

答案 2 :(得分:11)

根据英特尔Part 3A - System Programming Guideprocessor manuals第7章,如果在64位边界,Pentium或更新版本上对齐,并且未对齐(如果仍在缓存线)在P6或更新版本上。您应该使用volatile来确保编译器不会尝试将写入缓存在变量中,并且您可能需要使用内存栅栏例程来确保以正确的顺序进行写入。

如果您需要将写入现有值的值作为基础,则应使用操作系统的Interlocked功能(例如,Windows具有InterlockedIncrement64)。

答案 3 :(得分:10)

在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浏览器中找到一些实现。

答案 4 :(得分:6)

在X86上,以原子方式写入对齐的64位值的最快方法是使用FISTP。对于未对齐的值,您需要使用CAS2(_InterlockedExchange64)。由于BUSLOCK,CAS2操作非常慢,因此通常可以更快地检查对齐并为对齐的地址执行FISTP版本。实际上,这就是Intel Threaded building Blocks实现Atomic 64位写入的方式。

答案 5 :(得分:2)

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

“记忆围栏”或“记忆障碍”是您可能想要研究的术语。

答案 6 :(得分:1)

GCC拥有原子操作的内在函数;我怀疑你也可以和其他编译器做类似的事情。永远不要依赖编译器进行原子操作;除非你明确地告诉编译器不这样做,否则优化几乎肯定会冒着将非常明显的原子操作变成非原子操作的风险。

答案 7 :(得分:1)

最新版本的ISO C(C11)定义了一组原子操作,包括atomic_store(_explicit)。参见例如this page了解更多信息。

原子的第二个最便携的实现是GCC内在函数,已经提到过。我发现它们得到了GCC,Clang,Intel和IBM编译器的全面支持,并且 - 在我最后一次检查时 - 得到了Cray编译器的部分支持。

C11原子的一个明显优势 - 除了整个ISO标准之外 - 是它们支持更精确的记忆一致性处方。据我所知,GCC原​​子意味着一个完整的记忆障碍。