【问题标题】:How to guarantee 64-bit writes are atomic?如何保证 64 位写入是原子的?
【发布时间】:2010-09-09 20:20:21
【问题描述】:

在基于 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 位写入将是原子的。总是这样吗?

【问题讨论】:

    标签: c multithreading macos atomic lock-free


    【解决方案1】:

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

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

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

    【讨论】:

      【解决方案2】:

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

      【讨论】:

      【解决方案3】:

      在 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()。对于其他系统,您可能需要使用汇编,您可以在 SVN 浏览器中找到 HaikuOS 项目的一些实现,在 src/system/libroot/os/arch 中。

      【讨论】:

      • 为了阅读,你可以使用更简单的方法OSAtomicAdd64Barrier(0, target),它自动将0添加到目标指向的变量并返回加法的结果,在这种情况下是*目标本身
      【解决方案4】:

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

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

      【讨论】:

      • mfence 对于生产者-消费者队列来说太过分了。您只需要生产者端的sfence 和消费者端的lfence。没有题为“被认为有害的记忆障碍”的文章,但应该有:-)
      【解决方案5】:

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

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

      【讨论】:

      【解决方案6】:

      最好的办法是避免尝试用原语构建自己的系统,而是使用锁定,除非它真的在分析时显示为热点。 (如果你认为你可以聪明并避免使用锁,那就不要。你不是。那是一般的“你”,包括我和其他人。)你至少应该使用自旋锁,请参阅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 上正常工作。

      【讨论】:

      • 感谢您提供这样一个温暖而模糊的地方来发送未来的“实用”无锁福音派 :) + 10 如果可以的话。
      • 为什么不使用内存屏障而不是spinlockstackoverflow.com/questions/19965076/…
      【解决方案7】:

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

      【讨论】:

        【解决方案8】:

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

        【讨论】:

        • 您建议使用 GCC 内在函数,然后说不信任编译器。您指的是不是编译器不应该信任的内在函数?
        猜你喜欢
        • 2019-03-28
        • 2010-12-30
        • 2023-04-01
        • 1970-01-01
        • 1970-01-01
        • 2016-11-04
        • 1970-01-01
        • 1970-01-01
        • 2015-01-25
        相关资源
        最近更新 更多