20

当在基于 Intel x86 的平台(特别是基于 Intel 的 Mac 使用 Intel 编译器运行 MacOSX 10.4)上用 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 位写入将是原子的。这总是正确的吗?

4

8 回答 8

44

最好的办法是避免尝试用原语构建自己的系统,而是使用锁定,除非它在分析时确实显示为热点。(如果您认为自己可以聪明并避免使用锁,请不要这样做。您不是。那是一般的“您”,包括我和其他所有人。)您至少应该使用自旋锁,请参阅spinlock(3)。无论你做什么,都不要试图实现“你自己的”锁。你会弄错的。

最终,您需要使用操作系统提供的任何锁定或原子操作。在所有情况下都完全正确地完成这些事情是极其困难的。通常它可能涉及诸如特定处理器特定版本的勘误表之类的知识。(“哦,那个处理器的 2.0 版没有在正确的时间进行缓存一致性窥探,它在 2.0.1 版中已修复,但在 2.0 版中您需要插入一个.”)只需在 C 中的变量上添加一个关键字几乎总是不足的。NOPvolatile

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

于 2008-09-16T23:35:50.803 回答
13

在 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 页或附近)。

于 2008-09-16T23:26:04.727 回答
11

根据Intel处理器手册第 3A 部分 - System Programming Guide的第 7 章,如果在 Pentium 或更新版本的 64 位边界上对齐,并且在高速缓存行中未对齐(如果仍在高速缓存行内),四字访问将自动执行。 P6 或更高版本。您应该使用以确保编译器不会尝试将写入缓存在变量中,并且您可能需要使用内存围栏例程来确保写入以正确的顺序发生。volatile

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

于 2008-09-16T23:39:53.477 回答
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 浏览器中的一些实现。

于 2008-09-16T23:49:14.093 回答
6

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

于 2009-10-30T19:33:45.640 回答
3

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

第二个最可移植的原子实现是 GCC 内在函数,我们已经提到过。我发现 GCC、Clang、Intel 和 IBM 编译器完全支持它们,并且 - 截至我上次检查时 - Cray 编译器部分支持它们。

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

于 2015-04-06T22:20:29.023 回答
2

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

“内存栅栏”或“内存屏障”是您可能想要研究的术语。

于 2008-09-16T23:41:31.930 回答
1

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

于 2008-09-16T23:18:28.880 回答