Skip to content

Latest commit

 

History

History
550 lines (396 loc) · 32.3 KB

File metadata and controls

550 lines (396 loc) · 32.3 KB

八、原子操作——使用硬件

许多优化和线程安全取决于一个人对底层硬件的理解:从某些架构上的对齐内存访问,到知道哪些数据大小和 C++ 类型可以安全地处理,而不会影响性能或需要互斥体等。

本章介绍如何利用多种处理器体系结构的特性,例如,在原子操作可以防止任何访问冲突的情况下,防止使用互斥锁。编译器特定的扩展,比如 GCC 中的扩展,也会被检查。

本章的主题包括:

  • 原子操作的类型以及如何使用它们
  • 如何针对特定的处理器架构
  • 基于编译器的原子操作

原子操作

简而言之,原子操作是处理器可以用单个指令执行的操作。这使得它具有原子性,因为没有任何东西(除了中断)可以干扰它,或者改变它可能使用的任何变量或数据。

应用包括保证指令执行的顺序、无锁实现以及指令执行顺序和内存访问保证很重要的相关用途。

在 2011 年 C++ 标准之前,处理器提供的对这种原子操作的访问仅由编译器使用扩展来提供。

Visual C++

对于微软的 MSVC 编译器,有一些互锁的函数,如 MSDN 文档中所总结的,从添加特性开始:

| 联锁功能 | 描述 | | InterlockedAdd | 对指定的LONG值执行原子加法运算。 | | InterlockedAddAcquire | 对指定的LONG值执行原子加法运算。使用获取内存排序语义来执行该操作。 | | InterlockedAddRelease | 对指定的LONG值执行原子加法运算。该操作使用释放内存排序语义来执行。 | | InterlockedAddNoFence | 对指定的LONG值执行原子加法运算。操作以原子方式执行,但不使用内存屏障(本章将介绍)。 |

这些是该功能的 32 位版本。API 中也有这个方法和其他方法的 64 位版本。原子函数倾向于关注特定的变量类型,但是为了简洁起见,这个概要中省略了这个 API 的变体。

我们还可以看到获取和发布的变化。这些保证了相应的读或写访问将受到保护,不会因任何后续的读或写操作而导致存储器重新排序(在硬件级别上)。最后,无围栏变体(也称为内存屏障)在不使用任何内存屏障的情况下执行操作。

通常情况下,中央处理器执行指令(包括内存读取和写入)的顺序不对,以优化性能。由于这种类型的行为并不总是令人满意的,所以添加了内存屏障来防止这种指令重新排序。

接下来是原子AND特性:

联锁功能 描述
InterlockedAnd 对指定的LONG值执行原子AND操作。
InterlockedAndAcquire 对指定的LONG值执行原子AND操作。使用获取内存排序语义来执行该操作。
InterlockedAndRelease 对指定的LONG值执行原子AND操作。该操作使用释放内存排序语义来执行。
InterlockedAndNoFence 对指定的LONG值执行原子AND操作。操作以原子方式执行,但不使用内存屏障。

位测试功能如下:

联锁功能 描述
InterlockedBitTestAndComplement 测试指定LONG值的指定位并对其进行补充。
InterlockedBitTestAndResetAcquire 测试指定LONG值的指定位,并将其设置为0。操作为atomic,使用获取内存排序语义执行。
InterlockedBitTestAndResetRelease 测试指定LONG值的指定位,并将其设置为0。操作为atomic,使用内存释放语义执行。
InterlockedBitTestAndSetAcquire 测试指定LONG值的指定位,并将其设置为1。操作为atomic,使用获取内存排序语义执行。
InterlockedBitTestAndSetRelease 测试指定LONG值的指定位,并将其设置为1。操作为atomic,执行释放内存排序语义。
InterlockedBitTestAndReset 测试指定LONG值的指定位,并将其设置为0
InterlockedBitTestAndSet 测试指定LONG值的指定位,并将其设置为1

比较特征可以如下所示列出:

联锁功能 描述
InterlockedCompareExchange 对指定值执行原子比较和交换操作。该函数比较两个指定的 32 位值,并根据比较结果与另一个 32 位值进行交换。
InterlockedCompareExchangeAcquire 对指定值执行原子比较和交换操作。该函数比较两个指定的 32 位值,并根据比较结果与另一个 32 位值进行交换。使用获取内存排序语义来执行该操作。
InterlockedCompareExchangeRelease 对指定值执行原子比较和交换操作。该函数比较两个指定的 32 位值,并根据比较结果与另一个 32 位值进行交换。交换是用释放内存排序语义来执行的。
InterlockedCompareExchangeNoFence 对指定值执行原子比较和交换操作。该函数比较两个指定的 32 位值,并根据比较结果与另一个 32 位值进行交换。操作以原子方式执行,但不使用内存屏障。
InterlockedCompareExchangePointer 对指定的指针值执行原子比较和交换操作。该函数比较两个指定的指针值,并根据比较结果与另一个指针值进行交换。
InterlockedCompareExchangePointerAcquire 对指定的指针值执行原子比较和交换操作。该函数比较两个指定的指针值,并根据比较结果与另一个指针值进行交换。使用获取内存排序语义来执行该操作。
InterlockedCompareExchangePointerRelease 对指定的指针值执行原子比较和交换操作。该函数比较两个指定的指针值,并根据比较结果与另一个指针值进行交换。该操作使用释放内存排序语义来执行。
InterlockedCompareExchangePointerNoFence 对指定值执行原子比较和交换操作。该函数比较两个指定的指针值,并根据比较结果与另一个指针值进行交换。操作以原子方式执行,但不使用内存屏障

减量功能包括:

联锁功能 描述
InterlockedDecrement 将指定的 32 位变量的值递减(减一)为atomic操作。
InterlockedDecrementAcquire 将指定的 32 位变量的值递减(减一)为atomic操作。使用获取内存排序语义来执行该操作。
InterlockedDecrementRelease 将指定的 32 位变量的值递减(减一)为atomic操作。该操作使用释放内存排序语义来执行。
InterlockedDecrementNoFence 将指定的 32 位变量的值递减(减一)为atomic操作。操作以原子方式执行,但不使用内存屏障。

交换(交换)功能包括:

联锁功能 描述
InterlockedExchange 将 32 位变量设置为指定值作为atomic操作。
InterlockedExchangeAcquire 将 32 位变量设置为指定值作为atomic操作。使用获取内存排序语义来执行该操作。
InterlockedExchangeNoFence 将 32 位变量设置为指定值作为atomic操作。操作以原子方式执行,但不使用内存屏障。
InterlockedExchangePointer 原子地交换一对指针值。
InterlockedExchangePointerAcquire 原子地交换一对指针值。使用获取内存排序语义来执行该操作。
InterlockedExchangePointerNoFence 原子地交换一对地址。操作以原子方式执行,但不使用内存屏障。
InterlockedExchangeSubtract 执行两个值的原子减法。
InterlockedExchangeAdd 执行两个 32 位值的原子加法。
InterlockedExchangeAddAcquire 执行两个 32 位值的原子加法。使用获取内存排序语义来执行该操作。
InterlockedExchangeAddRelease 执行两个 32 位值的原子加法。该操作使用释放内存排序语义来执行。
InterlockedExchangeAddNoFence 执行两个 32 位值的原子加法。操作以原子方式执行,但不使用内存屏障。

增量功能包括:

联锁功能 描述
InterlockedIncrement 将指定的 32 位变量的值递增(增加 1)作为atomic操作。
InterlockedIncrementAcquire 将指定的 32 位变量的值递增(增加 1)作为atomic操作。使用获取内存排序语义来执行该操作。
InterlockedIncrementRelease 将指定的 32 位变量的值递增(增加 1)作为atomic操作。使用释放内存排序语义来执行该操作。
InterlockedIncrementNoFence 将指定的 32 位变量的值递增(增加 1)作为atomic操作。操作以原子方式执行,但不使用内存屏障。

OR功能:

联锁功能 描述
InterlockedOr 对指定的LONG值执行原子OR操作。
InterlockedOrAcquire 对指定的LONG值执行原子OR操作。使用获取内存排序语义来执行该操作。
InterlockedOrRelease 对指定的LONG值执行原子OR操作。该操作使用释放内存排序语义来执行。
InterlockedOrNoFence 对指定的LONG值执行原子OR操作。操作以原子方式执行,但不使用内存屏障。

最后,专属的OR ( XOR)功能有:

联锁功能 描述
InterlockedXor 对指定的LONG值执行原子XOR操作。
InterlockedXorAcquire 对指定的LONG值执行原子XOR操作。使用获取内存排序语义来执行该操作。
InterlockedXorRelease 对指定的LONG值执行原子XOR操作。该操作使用释放内存排序语义来执行。
InterlockedXorNoFence 对指定的LONG值执行原子XOR操作。操作以原子方式执行,但不使用内存屏障。

(同 groundcontrolcenter)地面控制中心

和 Visual C++ 一样,GCC 也自带一套内置的原子函数。这些基于 GCC 版本和标准库使用的底层架构而不同。由于 GCC 在比 VC++ 多得多的平台和操作系统上使用,在考虑可移植性时,这肯定是一个很大的因素。

例如,不是 x86 平台上提供的每个内置原子功能都可以在 ARM 上使用,部分原因是架构差异,包括特定 ARM 架构的变化。例如,ARMv6、ARMv7 或当前的 ARMv8,以及 Thumb 指令集,等等。

在 C++ 11 标准之前,GCC 使用了原子的__sync-prefixed扩展:

type __sync_fetch_and_add (type *ptr, type value, ...) 
type __sync_fetch_and_sub (type *ptr, type value, ...) 
type __sync_fetch_and_or (type *ptr, type value, ...) 
type __sync_fetch_and_and (type *ptr, type value, ...) 
type __sync_fetch_and_xor (type *ptr, type value, ...) 
type __sync_fetch_and_nand (type *ptr, type value, ...) 

这些操作从内存中获取一个值,并对其执行指定的操作,返回内存中的值。这些都使用记忆屏障。

type __sync_add_and_fetch (type *ptr, type value, ...) 
type __sync_sub_and_fetch (type *ptr, type value, ...) 
type __sync_or_and_fetch (type *ptr, type value, ...) 
type __sync_and_and_fetch (type *ptr, type value, ...) 
type __sync_xor_and_fetch (type *ptr, type value, ...) 
type __sync_nand_and_fetch (type *ptr, type value, ...) 

这些操作与第一组类似,只是它们在指定的操作之后返回新值。

bool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...) 
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...) 

如果旧值与提供的值匹配,这些比较操作将写入新值。如果新值已经写入,布尔变量返回真。

__sync_synchronize (...) 

这个函数创建了一个完整的内存屏障。

type __sync_lock_test_and_set (type *ptr, type value, ...) 

这个方法实际上是一个交换操作,不像它的名字所暗示的那样。它更新指针值并返回前一个值。这使用的不是完全内存屏障,而是获取屏障,这意味着它不会释放屏障。

void __sync_lock_release (type *ptr, ...) 

此函数释放由前面的方法获得的屏障。

为了适应 C++ 11 内存模型,GCC 增加了__atomic内置方法,这也大大改变了 API:

type __atomic_load_n (type *ptr, int memorder) 
void __atomic_load (type *ptr, type *ret, int memorder) 
void __atomic_store_n (type *ptr, type val, int memorder) 
void __atomic_store (type *ptr, type *val, int memorder) 
type __atomic_exchange_n (type *ptr, type val, int memorder) 
void __atomic_exchange (type *ptr, type *val, type *ret, int memorder) 
bool __atomic_compare_exchange_n (type *ptr, type *expected, type desired, bool weak, int success_memorder, int failure_memorder) 
bool __atomic_compare_exchange (type *ptr, type *expected, type *desired, bool weak, int success_memorder, int failure_memorder) 

首先是通用的加载、存储和交换函数。它们相当不言自明。加载函数在内存中读取一个值,存储函数在内存中存储一个值,交换函数用一个新值交换现有值。比较和交换函数使交换成为条件。

type __atomic_add_fetch (type *ptr, type val, int memorder) 
type __atomic_sub_fetch (type *ptr, type val, int memorder) 
type __atomic_and_fetch (type *ptr, type val, int memorder) 
type __atomic_xor_fetch (type *ptr, type val, int memorder) 
type __atomic_or_fetch (type *ptr, type val, int memorder) 
type __atomic_nand_fetch (type *ptr, type val, int memorder) 

这些函数本质上与旧的应用编程接口相同,返回特定操作的结果。

type __atomic_fetch_add (type *ptr, type val, int memorder) 
type __atomic_fetch_sub (type *ptr, type val, int memorder) 
type __atomic_fetch_and (type *ptr, type val, int memorder) 
type __atomic_fetch_xor (type *ptr, type val, int memorder) 
type __atomic_fetch_or (type *ptr, type val, int memorder) 
type __atomic_fetch_nand (type *ptr, type val, int memorder) 

同样,为新的应用编程接口更新了相同的功能。这些返回原始值(在操作之前获取)。

bool __atomic_test_and_set (void *ptr, int memorder) 

与旧 API 中类似命名的函数不同,该函数执行真正的测试和设置操作,而不是旧 API 函数的交换操作,这仍然需要一个函数在之后释放内存屏障。测试是针对某个定义的值。

void __atomic_clear (bool *ptr, int memorder) 

该功能清除指针地址,将其设置为0

void __atomic_thread_fence (int memorder) 

使用这个函数可以创建线程之间的同步内存屏障。

void __atomic_signal_fence (int memorder) 

这个函数在一个线程和同一个线程中的信号处理程序之间创建了一个内存屏障。

bool __atomic_always_lock_free (size_t size, void *ptr) 

该函数检查指定大小的对象是否总是为当前处理器体系结构创建无锁原子指令。

bool __atomic_is_lock_free (size_t size, void *ptr) 

这与前面的功能基本相同。

记忆顺序

在用于原子操作的 C++ 11 内存模型中,内存屏障(栅栏)并不总是被使用。在 GCC 内置的 atomics API 中,这反映在其函数中的memorder参数中。这个的可能值直接映射到 C++ 11 原子应用编程接口中的值:

  • __ATOMIC_RELAXED:表示没有线程间排序约束。
  • __ATOMIC_CONSUME:由于 C++ 11 对memory_order_consume的语义存在缺陷,目前使用更强的__ATOMIC_ACQUIRE内存顺序来实现。
  • __ATOMIC_ACQUIRE:从发布(或更强的)语义存储中创建线程间先发生约束来获取负载
  • __ATOMIC_RELEASE:创建线程间先发生约束,以获取(或更强的)从这个发布存储读取的语义负载
  • __ATOMIC_ACQ_REL:结合了__ATOMIC_ACQUIRE__ATOMIC_RELEASE的效果。
  • __ATOMIC_SEQ_CST:强制所有其他__ATOMIC_SEQ_CST操作的总排序。

前面的列表是从 GCC 手册中关于 GCC 7.1 原子的章节中复制过来的。连同那一章中的评论,它非常清楚地表明,在内存模型和编译器的实现中实现 C++ 11 atomics 支持时进行了权衡。

由于 atomics 依赖于底层的硬件支持,因此使用 atomics 的代码绝不会有一段能够在各种各样的体系结构中工作。

其他编译器

当然,C/C++ 的编译器工具链比 VC++ 和 GCC 多得多,包括英特尔编译器集合(ICC)和其他通常是专有的工具..这些都有自己的内置原子函数集合。幸运的是,由于 C++ 11 标准,我们现在有了一个完全可移植的编译器间原子标准。一般来说,这意味着在非常具体的用例(或现有代码的维护)之外,人们将使用 C++ 标准而不是编译器特定的扩展。

C++ 11 原子

为了使用原生的 C++ 11 atomics 特性,你所要做的就是包含<atomic>头。这使得atomic类可用,该类使用模板使自己适应所需的类型,并具有大量预定义的类型定义:

| 类型定义名称 | 完全特殊化 | | std::atomic_bool | std::atomic<bool> | | std::atomic_char | std::atomic<char> | | std::atomic_schar | std::atomic<signed char> | | std::atomic_uchar | std::atomic<unsigned char> | | std::atomic_short | std::atomic<short> | | std::atomic_ushort | std::atomic<unsigned short> | | std::atomic_int | std::atomic<int> | | std::atomic_uint | std::atomic<unsigned int> | | std::atomic_long | std::atomic<long> | | std::atomic_ulong | std::atomic<unsigned long> | | std::atomic_llong | std::atomic<long long> | | std::atomic_ullong | std::atomic<unsigned long long> | | std::atomic_char16_t | std::atomic<char16_t> | | std::atomic_char32_t | std::atomic<char32_t> | | std::atomic_wchar_t | std::atomic<wchar_t> | | std::atomic_int8_t | std::atomic<std::int8_t> | | std::atomic_uint8_t | std::atomic<std::uint8_t> | | std::atomic_int16_t | std::atomic<std::int16_t> | | std::atomic_uint16_t | std::atomic<std::uint16_t> | | std::atomic_int32_t | std::atomic<std::int32_t> | | std::atomic_uint32_t | std::atomic<std::uint32_t> | | std::atomic_int64_t | std::atomic<std::int64_t> | | std::atomic_uint64_t | std::atomic<std::uint64_t> | | std::atomic_int_least8_t | std::atomic<std::int_least8_t> | | std::atomic_uint_least8_t | std::atomic<std::uint_least8_t> | | std::atomic_int_least16_t | std::atomic<std::int_least16_t> | | std::atomic_uint_least16_t | std::atomic<std::uint_least16_t> | | std::atomic_int_least32_t | std::atomic<std::int_least32_t> | | std::atomic_uint_least32_t | std::atomic<std::uint_least32_t> | | std::atomic_int_least64_t | std::atomic<std::int_least64_t> | | std::atomic_uint_least64_t | std::atomic<std::uint_least64_t> | | std::atomic_int_fast8_t | std::atomic<std::int_fast8_t> | | std::atomic_uint_fast8_t | std::atomic<std::uint_fast8_t> | | std::atomic_int_fast16_t | std::atomic<std::int_fast16_t> | | std::atomic_uint_fast16_t | std::atomic<std::uint_fast16_t> | | std::atomic_int_fast32_t | std::atomic<std::int_fast32_t> | | std::atomic_uint_fast32_t | std::atomic<std::uint_fast32_t> | | std::atomic_int_fast64_t | std::atomic<std::int_fast64_t> | | std::atomic_uint_fast64_t | std::atomic<std::uint_fast64_t> | | std::atomic_intptr_t | std::atomic<std::intptr_t> | | std::atomic_uintptr_t | std::atomic<std::uintptr_t> | | std::atomic_size_t | std::atomic<std::size_t> | | std::atomic_ptrdiff_t | std::atomic<std::ptrdiff_t> | | std::atomic_intmax_t | std::atomic<std::intmax_t> | | std::atomic_uintmax_t | std::atomic<std::uintmax_t> |

这个atomic类定义了以下通用函数:

| 功能 | 描述 | | operator= | 为原子对象赋值。 | | is_lock_free | 如果原子对象是无锁的,则返回 true。 | | store | 以原子方式用非原子参数替换原子对象的值。 | | load | 原子地获取原子对象的值。 | | operator T | 从原子对象加载值。 | | exchange | 用新值自动替换对象的值,并返回旧值。 | | compare_exchange_weak``compare_exchange_strong | 自动比较对象的值,如果相等则交换值,否则返回当前值。 |

随着 C++ 17 的更新,增加了is_always_lock_free常量。这允许查询类型是否总是无锁的。

最后,我们有专门的atomic功能:

| 功能 | 描述 | | fetch_add | 自动将参数添加到存储在atomic对象中的值,并返回旧值。 | | fetch_sub | 从存储在atomic对象中的值中自动减去参数,并返回旧值。 | | fetch_and | 自动在参数和atomic对象的值之间执行按位AND,并返回旧值。 | | fetch_or | 自动在参数和atomic对象的值之间执行按位OR,并返回旧值。 | | fetch_xor | 自动在参数和atomic对象的值之间执行按位XOR,并返回旧值。 | | operator++ ``operator++(int)``operator--``operator--(int) | 将原子值递增或递减 1。 | | operator+=``operator-=``operator&=``operator&#124;=``operator^= | 用原子值进行加法、减法或按位ANDORXOR运算。 |

例子

使用fetch_add的基本示例如下所示:

#include <iostream> 
#include <thread> 
#include <atomic> 

std::atomic<long long> count; 
void worker() { 
         count.fetch_add(1, std::memory_order_relaxed); 
} 

int main() { 
         std::thread t1(worker); 
         std::thread t2(worker); 
         std::thread t3(worker); 
         std::thread t4(worker); 
         std::thread t5(worker); 

         t1.join(); 
         t2.join(); 
         t3.join(); 
         t4.join(); 
         t5.join(); 

         std::cout << "Count value:" << count << '\n'; 
} 

这个示例代码的结果将是5。正如我们在这里看到的,我们可以用 atomics 以这种方式实现一个基本的计数器,而不是为了提供线程同步而必须使用任何互斥或类似的东西。

非类函数

除了atomic类,在<atomic>头中还定义了许多基于模板的函数,我们可以用更类似于编译器内置原子函数的方式来使用这些函数:

| 功能 | 描述 | | atomic_is_lock_free | 检查原子类型的操作是否是无锁的。 | | atomic_storeatomic_store_explicit | 用非原子参数自动替换atomic对象的值。 | | atomic_load``atomic_load_explicit | 自动获取存储在atomic对象中的值。 | | atomic_exchange``atomic_exchange_explicit | 用非原子参数自动替换atomic对象的值,并返回atomic的旧值。 | | atomic_compare_exchange_weak``atomic_compare_exchange_weak_explicit``atomic_compare_exchange_strong``atomic_compare_exchange_strong_explicit | 自动将atomic对象的值与非原子参数进行比较,如果相等,则执行原子交换;如果不相等,则执行atomic加载。 | | atomic_fetch_add``atomic_fetch_add_explicit | 向atomic对象添加非原子值,并获得atomic的前一个值。 | | atomic_fetch_sub``atomic_fetch_sub_explicit | 从atomic对象中减去一个非原子值,得到atomic的前一个值。 | | atomic_fetch_and``atomic_fetch_and_explicit | 用非原子参数替换逻辑AND结果的atomic对象,得到原子的前一个值。 | | atomic_fetch_or``atomic_fetch_or_explicit | 用非原子参数替换逻辑OR结果的atomic对象,得到atomic的前一个值。 | | atomic_fetch_xor``atomic_fetch_xor_explicit | 用非原子参数替换逻辑XOR结果的atomic对象,得到atomic的前一个值。 | | atomic_flag_test_and_set``atomic_flag_test_and_set_explicit | 自动将标志设置为true并返回其先前值。 | | atomic_flag_clear``atomic_flag_clear_explicit | 自动将标志的值设置为false。 | | atomic_init | 默认构造的atomic对象的非原子初始化。 | | kill_dependency | 从std::memory_order_consume依赖关系树中移除指定的对象。 | | atomic_thread_fence | 通用内存顺序相关的围栏同步原语。 | | atomic_signal_fence | 线程和在同一线程中执行的信号处理程序之间的隔离。 |

常规函数和显式函数的区别在于,后者允许用户实际设置要使用的内存顺序。前者总是用memory_order_seq_cst作为记忆顺序。

例子

在这个使用atomic_fetch_sub的例子中,一个索引容器由多个线程并发处理,不使用锁:

#include <string> 
#include <thread> 
#include <vector> 
#include <iostream> 
#include <atomic> 
#include <numeric> 

const int N = 10000; 
std::atomic<int> cnt; 
std::vector<int> data(N); 

void reader(int id) { 
         for (;;) { 
               int idx = atomic_fetch_sub_explicit(&cnt, 1, std::memory_order_relaxed); 
               if (idx >= 0) { 
                           std::cout << "reader " << std::to_string(id) << " processed item " 
                                       << std::to_string(data[idx]) << '\n'; 
               }  
         else { 
                           std::cout << "reader " << std::to_string(id) << " done.\n"; 
                           break; 
               } 
         } 
} 

int main() { 
         std::iota(data.begin(), data.end(), 1); 
         cnt = data.size() - 1; 

         std::vector<std::thread> v; 
         for (int n = 0; n < 10; ++ n) { 
               v.emplace_back(reader, n); 
         } 

         for (std::thread& t : v) { 
               t.join(); 
         } 
} 

这个示例代码使用一个填充了大小为 N 的整数的向量作为数据源,用 1 填充它。原子计数器对象被设置为数据向量的大小。之后,创建 10 个线程(使用向量的emplace_back C++ 11 特性就地初始化),运行reader函数。

在该函数中,我们使用atomic_fetch_sub_explicit函数从内存中读取索引计数器的当前值,这允许我们使用memory_order_relaxed内存顺序。这个函数还从这个旧值中减去我们传递的值,将索引向下计数 1。

只要我们通过这种方式得到的索引号高于或等于零,函数就继续,否则就会退出。一旦所有线程完成,应用就退出。

原子旗帜

std::atomic_flag是原子布尔类型。与atomic类的其他专门化不同,它保证是无锁的。但是,它不提供任何加载或存储操作。

取而代之的是,它提供赋值操作符,并用于清除或test_and_set标志。前者由此将标志设置为false,后者将测试并将其设置为true

记忆顺序

该属性被定义为<atomic>标题中的枚举:

enum memory_order { 
    memory_order_relaxed, 
    memory_order_consume, 
    memory_order_acquire, 
    memory_order_release, 
    memory_order_acq_rel, 
    memory_order_seq_cst 
}; 

在 GCC 部分,我们已经简单地谈到了记忆顺序的话题。如上所述,这是底层硬件体系结构的特征在某种程度上显现出来的部分之一。

基本上,内存顺序决定了围绕原子操作的非原子内存访问的顺序(内存访问顺序)。这会影响不同线程在执行指令时如何看到内存中的数据:

| 枚举 | 描述 | | memory_order_relaxed | 宽松操作:没有对其他读取或写入施加同步或排序约束,只有这个操作的原子性得到保证。 | | memory_order_consume | 具有此内存顺序的加载操作在受影响的内存位置上执行消耗操作:根据当前加载的值,当前线程中的任何读或写都不能在此加载之前重新排序。对释放相同原子变量的其他线程中的数据相关变量的写入在当前线程中可见。在大多数平台上,这只会影响编译器优化。 | | memory_order_acquire | 具有此内存顺序的加载操作在受影响的内存位置上执行获取操作:在此加载之前,当前线程中的任何读或写都不能被重新排序。释放相同原子变量的其他线程中的所有写入在当前线程中都是可见的。 | | memory_order_release | 具有该存储顺序的存储操作执行释放操作:在该存储之后,当前线程中的任何读或写都不能被重新排序。当前线程中的所有写操作在获取相同原子变量的其他线程中都是可见的,并且将依赖关系携带到原子变量中的写操作在使用相同原子变量的其他线程中变得可见。 | | memory_order_acq_rel | 具有该存储顺序的读-修改-写操作既是获取操作又是释放操作。当前线程中的任何内存读取或写入都不能在此存储之前或之后重新排序。在修改之前,释放相同原子变量的其他线程中的所有写入都是可见的,并且修改在获取相同原子变量的其他线程中是可见的。 | | memory_order_seq_cst | 任何具有此内存顺序的操作都是一个获取操作和一个释放操作,加上一个单一的总顺序,其中所有线程以相同的顺序观察所有修改。 |

宽松排序

使用宽松的内存排序,并发内存访问之间没有强制顺序。这种排序保证的只是原子性和修改顺序。

这种排序的典型用途是计数器,无论是递增还是递减,正如我们在前面的示例代码中看到的。

发布-获取订单

如果线程 A 中的原子存储被标记为memory_order_release,而线程 B 中来自同一变量的原子加载被标记为memory_order_acquire,那么从线程 A 的角度来看,在原子存储之前发生的所有内存写入(非原子的和宽松的原子的)都变成了线程 B 中的可见副作用*,也就是说,一旦原子加载完成,线程 B 保证会看到线程 A 写入内存的所有内容。*

这种类型的操作在所谓的强有序架构上是自动的,包括 x86、SPARC 和 POWER。弱有序架构,如 ARM、PowerPC 和 Itanium,将需要在这里使用内存屏障。

这种内存排序的典型应用包括互斥机制,如互斥或原子自旋锁。

发布-消费订购

如果线程 A 中的原子存储被标记为memory_order_release,而来自同一变量的线程 B 中的原子加载被标记为memory_order_consume,则从线程 A 的角度来看,原子存储之前的所有内存写入(非原子的和宽松的原子的)依赖排序的,在加载操作携带依赖的线程 B 中的那些操作中变成可见的副作用*。*也就是说,一旦原子加载完成,线程 B 中那些使用从加载中获得的值的操作符和函数就可以保证看到线程 A 写入内存的内容。

这种排序在几乎所有的架构上都是自动的。唯一的主要例外是(过时的)阿尔法架构。这种排序方式的一个典型用例是读取很少被更改的数据。

As of C++ 17, this type of memory ordering is being revised, and the use of memory_order_consume is temporarily discouraged.

顺序一致排序

标记为memory_order_seq_cst的原子操作不仅以与释放/获取排序相同的方式对内存进行排序(在一个线程中存储之前发生的所有事情在进行加载的线程中都变成了可见的副作用,而且还为所有标记为memory_order_seq_cst的原子操作建立了单一的总修改顺序

这种排序对于所有使用者都必须以完全相同的顺序观察其他线程所做的更改的情况可能是必要的。在多核或多 CPU 系统上,它需要完全的内存屏障。

由于如此复杂的设置,这种类型的排序明显慢于其他类型。它还要求每一个原子操作都必须用这种类型的内存排序进行标记,否则顺序排序将会丢失。

易变关键字

volatile这个关键词对于任何一个写过复杂多线程代码的人来说,可能都是相当熟悉的。它的基本用途是告诉编译器,相关变量应该总是从内存中加载,永远不要对其值做假设。它还确保编译器不会对变量进行任何激进的优化。

对于多线程应用,它通常是无效的,但是,不鼓励使用它。volatile 规范的主要问题是它没有定义多线程内存模型,这意味着这个关键字的结果可能不会跨平台、CPU 甚至工具链确定。

在原子领域,这个关键字不是必需的,事实上也不太可能有帮助。为了保证获得在多个中央处理器内核和它们的高速缓存之间共享的变量的当前版本,必须使用像atomic_compare_exchange_strongatomic_fetch_addatomic_exchange这样的操作来让硬件获取正确的当前值。

对于多线程代码,建议不要使用 volatile 关键字,而是使用 atomics,以保证正确的行为。

摘要

在这一章中,我们研究了原子操作,以及如何将它们集成到编译器中,以使代码尽可能地与底层硬件紧密合作。读者现在将熟悉原子操作的类型、内存屏障(栅栏)的使用,以及各种类型的内存排序及其含义。

读者现在能够在自己的代码中使用原子操作来完成无锁设计,并正确使用 C++ 11 内存模型。

在下一章中,我们将带着到目前为止所学的一切,远离 CPU,转而看一下 GPU,显卡(GPU)上数据的通用处理。