accdb 用户类型未定义_《C++并发编程实战第2版》第五章:C++内存模型与原子类型操作(1/4)...

注:本章内存模型术语较多,翻译出来稍显晦涩,建议本章从头开始阅读

本章主要内容

  • C++内存模型的细节
  • C++提供的原子类型
  • 标准库
  • 原子类型上的可用操作
  • 如何使用这些操作来提供线程间同步

C++标准中,有一个十分重要的特性,被大多数程序员所忽略。它不是一个新语法特性,也不是新的库设施,而是新的多线程感知内存模型。如果没有内存模型准确地定义基本构建块的工作方式,那我所介绍的所有设施都无法正常工作。有一个原因大多数程序员不会注意到:如果你使用互斥锁来保护你的数据,以及条件变量、期望、锁存器或屏障来用信号通知事件,那它们为什么有效的细节并不重要。只有当你开始尝试“接触硬件”(close to the machine)时,内存模型的精确细节才变得重要。

不管怎么说,C++是一个系统级别的编程语言,标准委员会的目标之一就是不需要比C++还要底层的高级语言。在C++中应该为程序员提供足够的灵活性来做他们需要的任何事情,而不受语言的阻碍,当需要的时候允许他们“接触硬件”。原子类型和操作正好允许这样做,为低层次同步操作提供的设施通常将减少到1~2个CPU指令。

在本章中,我将首先介绍内存模型的基础知识,然后介绍原子类型和操作,最后介绍原子类型操作中各种可用的同步类型。这相当复杂:除非你计划编写使用原子操作进行同步的代码(如第7章中的无锁数据结构),否则你不需要知道这些细节。

让我们先来看一下有关内存模型的基本知识。

5.1 内存模型基础

内存模型包含两个方面:基本结构(structural)方面,它与事物在内存中的布局有关,以及并发方面。结构方面对于并发(conrurrency)非常重要,特别是在考虑低层次原子操作的时候,因此我将从这块开始。在C++中,结构方面全部是关于对象和内存位置的内容。

5.1.1 对象和内存位置

C++程序中的所有数据都是由对象(objects)组成的。这并不是说你可以创建一个从int派生的新类,也不是说基本类型具有成员函数,也不是说在讨论Smalltalk或Ruby这样的语言时,当人们说所有东西都是对象时,通常会暗指的任何其他结果。它是关于C++中构建数据块的声明。C++标准将一个对象定义为“一个存储区域”(a region of storage),尽管它继续为这些对象分配属性,比如它们的类型和生命周期。

其中一些对象是基本类型,如int或float的简单值,而其他对象是用户自定义类的实例。有些对象(例如数组、派生类的实例和具有非静态数据成员的类的实例)有子对象,但其他对象没有。

无论对象的类型是什么,它都存储在一个或多个内存位置中。每个内存位置要么是标量类型(如unsigned short或my class*)的对象(或子对象),要么是相邻位域的序列。如果你使用位域,需要注意的重要一点是尽管相邻的位域是不同的对象,但它们仍然被算作相同的内存位置。图5.1显示了一个struct如何划分成对象和内存位置。

9afe7d78c238efc18d1836ab1239832b.png
图5.1 把一个struct分割为对象和内存位置

首先,整个struct是一个由几个子对象组成的对象,每个子对象对应一个数据成员。bf1和bf2位域共享一个内存位置,std::string对象s内部由几个内存位置组成,但除此之外,每个成员都有自己的内存位置。注意零长度位域bf3(bf3这个名字是用来注释用的,因为C++中零长度位域必须是未命名的)如何将bf4分隔到它自己的内存位置,但它本身没有内存位置。(译注:C++中零长度的未命名位域有特殊含义:让下一个位域从分配单元的边界开始。C++中位域的内存布局是与机器相关的,取地址操作符&不能作用于位域)。

从中我们可以了解到四件重要的事情:

  1. 每一个变量都是一个对象,包括作为其他对象成员的变量。
  2. 每个对象至少占有一个内存位置。
  3. 基本类型的变量,如int或char,无论它们的大小如何,即使它们是相邻的或数组的一部分,也只占用一个内存位置。
  4. 相邻位域是相同内存位置的一部分。

我敢肯定你想知道这与并发有什么关系,所以让我们来看一看。

5.1.2 对象、内存位置和并发

现在,这里是对C++中多线程应用程序至关重要的部分:一切都取决于那些内存位置。如果两个线程访问单独的内存位置,这没有问题:一切工作正常。另一方面,如果两个线程访问相同的内存位置,那你必须小心。如果两个线程都没有更新内存位置,那就没事;因为只读数据不需要保护或同步。如果任何一个线程正在修改数据,就可能出现竞争条件,如第3章所述。

为了避免竞争条件,两个线程的访问之间必须要有一个强制顺序。这可以是一个固定的顺序,这样一个访问总是在另一个之前,也可以是一个在应用程序的运行之间变化的顺序,但保证有某种已定义的顺序。确保有序的一种方法是使用第3章中描述的互斥锁;如果在两次访问之前都锁住了同一个互斥锁,那么每次只有一个线程可以访问该内存位置,所以一个必须在另一个之前发生(尽管,通常你没法提前知道谁先谁后)。另一种方法是在相同的或其他内存位置上使用原子操作的同步属性(关于原子操作的定义,请参阅5.2节)来强制两个线程的访问顺序。5.3节描述了使用原子操作来强制一个顺序。如果有两个以上的线程访问相同的内存位置,那么每对访问都必须有一个定义好的顺序。

如果从不同的线程对单个内存位置的两次访问之间没有强制顺序,那么其中一次或两次访问都不是原子性的,并且如果其中一次或两次访问都是写操作,那这就是数据竞争,会导致未定义的行为。

这个声明非常重要:未定义的行为是C++中最肮脏的角落之一。根据语言标准,一旦应用程序包含任何未定义的行为,将满盘皆输;整个应用程序的行为现在是未定义的,它可以做任何事情。我知道个例子,一个未定义的行为导致某人的监视器着火了。虽然这种情况不大可能发生在你身上,但数据竞争绝对是一个严重的错误,应该不惜一切代价加以避免。

该声明中还有另一个要点:你还可以通过使用原子操作访问竞争中涉及的内存位置来避免未定义的行为。这并不能防止竞争本身——哪个原子操作首先触碰内存位置仍然没有指定——但它确实将程序带回到已定义行为的领域。

在讨论原子操作之前,还有一个关于对象和内存位置的重要概念:修改顺序。

5.1.3 修改顺序

C++程序中的每个对象都有一个修改顺序(modification order),由程序中所有线程对该对象的所有写操作组成,始于对象初始化。在大多数情况下,这个顺序在运行时是不同的,但是在任何给定的程序执行中,系统中所有线程都必须对这个顺序达成一致。如果涉及的对象不是5.2节中描述的原子类型,那么你要负责确保有足够的同步,以保证线程对每个变量的修改顺序达成一致。如果不同的线程看到一个变量值的不同序列,那么就会出现数据竞争和未定义行为(参见5.1.2节)。如果你确实使用了原子操作,则编译器会负责确保必要的同步。

这一要求意味着某些类型的推测执行是不允许的,因为一旦一个线程看到了修改顺序中的特定条目,随后那个线程的读操作必须返回之后的值,并且那个线程随后对那个对象的写操作必须在修改顺序中更晚发生。同样,在同一个线程中,在对该对象进行写操作之后对该对象进行读操作,必须要么返回已写的值,要么返回在修改顺序中更晚出现的另一个值。虽然所有线程必须对程序中每个单独对象的修改顺序达成一致,但它们不必对不同对象的相对操作顺序达成一致。关于线程之间的操作顺序,请参阅5.3.3节。

那么,什么构成了一个原子操作?如何使用它们来强制顺序呢?

5.2 C++中的原子操作和类型

原子操作(atomic operation)是不可分割的操作。系统中任何线程都不可能观察到操作只完成一半;要么做了,要么没做。如果读取对象值的加载操作是原子的(atomic),并且对该对象的所有修改也是原子的(atomic),那么加载将检索到对象的初始值或其中一个修改所存储的值。

另一方面,非原子的操作可能被另一个线程观察到只完成一半。如果非原子操作是由原子操作(例如,赋值给成员是atomic的struct)组成,那么其他线程可能把某个组成原子操作的一个子集视作完整的,但实际上其他操作还没有开始,所以你可能会观察到,或最后得到一个值,这个值是各种存储值的混乱组合。在任何情况下,对非原子变量的非同步访问都会形成一个简单的有问题的竞争条件,如第3章所述,但是在这个层次上,它可能会构成一个数据竞争(data race)(参见5.1节)并导致未定义的行为。

在C++中,在大多数情况下,需要使用原子类型来获得原子操作,所以让我们来看看这些原子类型。

5.2.1 标准原子类型

标准原子类型(atomic types)可以在头文件<atomic>中找到。这些类型的所有操作都是原子的,并且从语言定义讲只有这些类型的操作是原子的,尽管可以使用互斥锁让其他操作看起来是原子的。实际上,标准原子类型它们本身就可能使用这样的仿真:它们(几乎)都有一个is_lock_free()成员函数,这个函数可以让用户查询给定类型上的操作是直接用原子指令(x.is_lock_free()返回true)实现的,还是通过使用编译器和库内部的锁实现的(x.is_lock_free()返回false)。

在很多情况下了解这一点是很重要的——原子操作的关键用例是替换那些使用互斥锁进行同步的操作;如果原子操作本身使用内部互斥锁,那么期望的性能增益可能不会实现,你最好使用更容易搞定的基于互斥锁的实现。第7章中讨论的无锁数据结构就是这种情况。

事实上,这一点是如此重要,以至于库提供了一组宏,用于在编译时识别各种整型的原子类型是否无锁。自从C++17以后,所有的原子类型都有一个静态的constexpr成员变量,X::is_always_lock_free,当且仅当原子类型X对于当前编译的输出在可能运行的所有支持的硬件上都是无锁的时候,这个变量的值才为true。例如,对于给定的目标平台,std::atomic<int>可能总是无锁的,所以std::atomic<int>::is_always_lock_free将为真,但是std::atomic<uintmax_t>可能只有在程序最终运行的硬件支持必要的指令时才无锁。所以这是一个运行时属性,std::atomic<uintmax_t>::is_always_lock_free在为该平台编译时将为false。

这些宏是ATOMIC_BOOL_LOCK_FREE、ATOMIC_CHAR_LOCK_FREE和ATOMIC_CHAR16_T_LOCK_FREE、ATOMIC_CHAR32_T_LOCK_FREE ATOMIC_WCHAR_T_LOCK_FREE, ATOMIC_SHORT_LOCK_FREE, ATOMIC_INT_LOCK_FREE, ATOMIC_LONG_LOCK_FREE, ATOMIC_LLONG_LOCK_FREE, ATOMIC_POINTER_LOCK_FREE。它们为指定的内置类型及其无符号相对物对应的原子类型指定了无锁状态(LLONG指long long,而POINTER指所有的指针类型)。如果原子类型从来都不是无锁的,则值为0;如果原子类型始终无锁,则值为2;如果相应的原子类型的无锁状态是前面描述的运行时属性,则值为1。

唯一不提供is_lock_free()成员函数的类型是std::atomic_flag。该类型是一个简单的布尔标志,并且在这种类型上的操作必须是无锁的;一旦你有了一个简单的无锁布尔标志,你就可以使用它来实现一个简单的锁,并使用它作为基础来实现所有其他原子类型。当我说简单时,我的意思是:std::atomic_flag类型的对象被初始化为清除状态,然后它们可以被查询和设置(使用test_and_set()成员函数)或者被清除(使用clear()成员函数)。

其余的原子类型都是通过std::atomic<>类模板的特化来获得,它们的功能更全面一些,但可能不是无锁的(前面解释过)。在大多数流行的平台上,可以预期所有内置类型的原子变体(例如std::atomic<int>和std::atomic::<void*>)的确是无锁的,但这不是必需的。你很快就会看到,每个特化的接口反映了类型的属性;例如,像&=这样的按位操作没有为普通指针定义,所以它们也没有为原子指针定义。

除了直接使用std::atomic<>类模板之外,还可以使用表5.1中所示的一组名称来引用实现提供的原子类型。由于原子类型添加到C++标准的历史,如果你有一个老的编译器,这些替代类型名称可能引用对应std::atomic<>的特化类或这个特化类的一个基类,而在一个完全支持C++17的编译器上,这些总是对应std::atomic<>特化类的别名。因此,在同一个程序中混合使用这些代用名和直接命名的std::atomic<>特化类可能会导致不可移植的代码。

8734abe7d2a1e4b9399e79fbda136c89.png

除了基本的原子类型之外,C++标准库还为原子类型提供了一组typedef对应各种非原子的标准库typedef,如std::size_t。如表5.2所示。

95547954497a64baaff63d399aa97f35.png

这里有很多类型!有一个非常简单的模式;对于标准的typedef T,对应的原子类型是相同的名字加上atomic_前缀:atomic_T。这种情况也适用于内置类型,只是有符号的缩写为s,无符号的缩写为u,long long的缩写为llong。对于任何T,比起使用代用名,通常std::atomic<T>更明了。

标准原子类型在传统意义上是不可复制或赋值的,因为它们没有拷贝构造函数或拷贝赋值操作符。但是,它们确实支持从相应的内置类型赋值以及隐式转换成内置类型,以及直接的load()和store()成员函数、exchange()、compare_exchange_weak()和compare_exchange_strong()。它们还在适当的地方支持复合赋值操作符:+=、-=、*=、|=等等,还有整型,以及用于支持++和—指针的std::atomic<>特化版本。这些操作符还有相应的有相同功能的命名成员函数:fetch_add()、fetch_or(),等等。赋值操作符和成员函数的返回值要么是存储的值(对于赋值操作符),要么是操作之前的值(对于命名的函数)。这就避免了潜在的问题,这个问题根源于赋值操作符通常习惯于返回被赋值对象的引用。为了从这些引用中获取存储的值,代码必须执行单独的读操作,从而允许另一个线程在赋值和读之间修改值,这就打开了竞争条件的大门。

但std::atomic<>类模板不仅仅是一组特化。它确实有一个主模板,可用于创建用户自定义类型的原子变体。因为它是一个泛型类模板,所以操作被限制为load()、store()(以及用户自定义类型的赋值和转换)、exchange()、compare_exchange_weak()和compare_exchange_strong()。

每个原子类型上的操作都有一个可选的内存顺序参数,它是std::memory_order枚举类型的某个值,这个参数可以用来指定所需的内存顺序语义。std::memory_order有六种可能的值:std::memory_order_relaxed,std::memory_order_acquire,std::memory_order_consume,std::memory_order_acq_rel,std::memory_order_release,std::memory_order_seq_cst

允许使用的内存顺序值依赖于操作的类别,如果你不指定内存顺序,默认使用std::memory_order_seq_cst,它是最强的内存顺序。5.3节讨论了内存顺序选项的精确语义。目前,只要知道操作分为三类就足够了:

  1. 存储(store)操作,可以有memory_order_relaxedmemory_order_release,或memory_order_seq_cst的顺序
  2. 加载(load)操作,可以有memory_order_relaxedmemory_order_consumememory_order_acquire,或memory_order_seq_cst顺序
  3. 读-改-写(read-modify-write)操作,可以有memory_order_relaxedmemory_order_consumememory_order_acquirememory_order_releasememory_order_acq_rel,或memory_order_seq_cst

现在,让我们来看一下每个标准原子类型上可以执行的操作,就从std::atomic_flag开始。

5.2.2 std::atomic_flag上的操作

std::atomic_flag是最简单的标准原子类型,它表示一个布尔标志。此类型的对象可以处于两种状态之一:设置状态或清除状态。它被刻意设计为基础的,并且仅仅作为一个构建块使用。因此,除了在特殊情况下,我从不期望看到它被使用。尽管如此,它仍将作为讨论其他原子类型的起点,因为它展示了应用于原子类型的一些通用策略。。

std::atomic_flag类型的对象必须用ATOMIC_FLAG_INIT初始化。这将标志初始化为清除状态。在这件事上没有选择;这个标志总是从清除状态开始:

std::atomic_flag f = ATOMIC_FLAG_INIT;

无论对象在哪里声明,它的作用域是什么,这都适用。它是唯一在初始化时需要这种特殊处理的原子类型,但它也是唯一保证无锁的类型。如果std::atomic_flag对象具有静态存储持续时间。它保证是静态初始化的,这意味着没有初始化顺序的问题;它总是在对标志进行第一次操作时被初始化。

一旦初始化标志对象后,只有三件事可以做:销毁它、清除它或设置它并查询之前的值。它们分别对应于析构函数、clear()成员函数和test_and_set()成员函数。clear()和test_and_set()成员函数两者都可以指定内存顺序。clear()是一个存储操作,因此不能有memory_order_acquire或memory_order_acq_rel语义,但是test_and_set()是一个读-改-写操作,因此可以应用任何内存顺序标签。与每个原子操作一样,这两个操作的默认值都是memory_order_seq_cst。例如:

f.clear(std::memory_order_release);  // 1
bool x=f.test_and_set();  // 2

调用clear()①明确要求使用释放语义清除标志,而调用test_and_set()②时使用默认内存顺序设置标记,并检索旧值。

你不能从第一个对象拷贝构造另一个std::atomic_flag对象,也不能从一个std::atomic_flag赋值给另一个。这不是对std::atomic_flag的特殊要求,而是所有原子类型通用的行为。原子类型上的所有操作都被定义为原子性的,赋值和拷贝构造涉及两个对象。对两个不同对象的单个操作不可能是原子性的。在拷贝构造或拷贝赋值的情况下,必须首先从一个对象读取值,然后将其写入另一个对象。这是两个独立对象上的两个独立操作,而且组合不可能是原子的。因此,不允许这些操作。

有限的特性使得std::atomic_flag非常适合于作自旋互斥锁。起初,标志被清除,并且互斥锁处于解锁状态。为了锁住互斥锁,循环运行test_and_set()直到旧值为false,这意味着这个线程已经把值设置为true了。解锁互斥锁是一件很简单的事情,将标志清除即可。实现如下面的程序清单所示:

dabe34155661e3ac76ba69fcba2eb693.png

这个互斥锁很基础,但是足以用于std::lock_guard<>(参见第3章)。由于它本质上是在lock()中做了一个忙-等待,所以如果你预期有任何程度的竞争,它是一个糟糕的选择,但它足以确保互斥。当我们研究内存顺序语义时,你将看到和互斥锁一起使用的时候,这如何保证所必需的强制顺序。这样的例子将在5.3.6节中介绍。

std::atomic_flag非常局限,以至于它甚至不能用作一般的布尔标志,因为它没有简单的非修改查询操作。为此,你最好使用std::atomic<bool>,因此我接下来讨论这个。

5.2.3 std::atomic<bool>上的操作

最基本的原子整型类型就是std::atomic<bool>。如你所料,它有着比std::atomic_flag更加齐全的布尔标志特性。虽然依旧不能拷贝构造和拷贝赋值,但可以使用非原子的bool类型构造它,所以它可以被初始化为true或false,并且可以从非原子bool变量赋值给std::atomic<bool>

std::atomic<bool> b(true);
b=false;

需要注意的是,非原子bool的赋值操作符不同于通常的惯例:返回被赋值对象的引用,取而代之,它返回要赋给的bool值。这是原子类型的另一个常见模式:它们的赋值操作符支持返回值(对应非原子类型的值),而不是引用。如果原子变量的引用被返回了,任何依赖于这个赋值结果的代码都需要显式加载这个值。潜在地可能获得另一个线程修改的结果。通过以非原子值的形式返回赋值结果,可以避免额外的加载操作,并且你得到的就是存储的值。

与使用std::atomic_flag受限的clear()函数不同,写入(true或者false)是通过调用store()来完成的,尽管仍然可以指定内存顺序语义。类似地,test_and_set()已被更通用的exchange()成员函数替换,该函数允许你将存储的值替换为你选择的新值,并自动检索原始值。std::atomic<bool>还支持通过隐式转换为简单的bool或通过显式调用load()对值进行非修改查询。如你所料,store()是一个存储操作,而load()是一个加载操作。exchange()是一个“读-改-写”操作:

std::atomic<bool> b;
bool x=b.load(std::memory_order_acquire);
b.store(true);
x=b.exchange(false, std::memory_order_acq_rel);

exchange()不是std::atomic<bool>所支持的唯一的读-改-写操作;它还引入了一个操作,以便在当前值等于预期值时存储一个新值。

根据当前值存储一个新值(或不存储)

这个新操作称为比较-交换,它以成员函数compare_exchange_weak()和compare_exchange_strong()的形式出现。比较-交换操作是使用原子类型进行编程的基石;它将原子变量的值与提供的预期值进行比较,如果它们相等,则存储提供的期望值。如果值不相等,则预期值会用原子变量的值更新。比较-交换函数的返回类型是bool类型,如果执行了存储,则为true,否则为false。如果存储完成(因为值相等),则操作成功,否则操作失败;返回值为true表示成功,返回值为false表示失败。

对于compare_exchange_weak()函数,即使原始值与预期值一致时,存储也可能会不成功;在这种情况中变量的值不会发生改变,并且compare_exchange_weak()的返回值是false。这最可能发生在缺少“比较并交换”指令的机器上,如果处理器不能保证操作原子地完成——可能是因为执行操作的线程在必要的指令序列中间被切换出去,而操作系统在其位置调度了另一个线程,在这里线程数多于处理器的数量。这被称为伪失败(spurious failure),因为失败的原因是时序的作用,而不是因为变量的值。

因为compare_exchange_weak()可以伪失败,所以通常必须在一个循环中使用:

bool expected=false;
extern atomic<bool> b; // 在其他某个地方设置
while(!b.compare_exchange_weak(expected,true) && !expected);

在本例中,只要expected仍然为false,就会继续循环,这表明compare_exchange_weak()调用伪失败了。

另一方面,只有当值不等于预期值时,compare_exchange_strong()保证返回false。这可以在像刚才展示的地方消除对循环的需求,在这些地方你想知道是否成功地更改了一个变量,或者另一个线程是否先到达了那里。

如果你想修改变量而不管初始值是什么(可能使用一个依赖于当前值的更新值),则expected的更新将变得很有用。在每次循环中,expected都会被重新加载,因此如果在此期间没有其他线程修改该值,则compare_exchange_weak()或compare_exchange_strong()调用在下一次循环中应该成功。如果要存储的值计算很简单,那么使用compare_exchange_weak可能是有益的,它避免在compare_exchange_weak()可能伪失败的平台上使用双循环(所以compare_exchange_strong()包含一个循环)。另一方面,如果要存储的值计算非常耗时,那么使用compare_exchange_strong()可以避免在expected值没有改变时重新计算要存储的值。对于std::atomic<bool>,这并不是很重要—毕竟只有两个可能的值—但是对于较大的原子类型,这可能会不一样。

比较交换函数的另一个不同寻常之处在于,它们可以使用两个内存顺序参数。这允许在成功和失败的情况下,有不同的内存顺序语义;对于一个成功的调用拥有memory_order_acq_rel语义,而失败的调用具有memory_order_relaxed语义是可取的。失败的比较交换不会做存储,因此它不能有memory_order_release或memory_order_acq_rel语义。因此,不允许为失败提供这些值作为顺序。你也不能为失败提供比成功更严格的内存顺序;如果你希望将memory_order_acquire或memory_order_ seq_cst语义用于失败,那么也必须为成功指定这些语义。

如果你没有指定失败的顺序,则假定它与成功的顺序相同,只是顺序的release部分被剥离:memory_order_release变成memory_order_relax, memory_order_acq_rel变成memory_order_acquire。如果两者都没有指定,则默认为memory_order_seq_cst,这将提供成功和失败的全序列顺序。下面两个对compare_exchange_weak()的调用是等效的:

std::atomic<bool> b;
bool expected;
b.compare_exchange_weak(expected,true,
  memory_order_acq_rel,memory_order_acquire);
b.compare_exchange_weak(expected,true,memory_order_acq_rel);

我将把选择内存顺序的结果留到5.3节讨论。

std::atomic<bool>和std::atomic_flag之间一个进一步的区别是std::atomic<bool>可能不是无锁的;实现可能必须在内部获得互斥锁,以确保操作的原子性。在这种情况下,可以使用is_lock_free()成员函数检查std::atomic<bool>上的操作是否无锁。这是除了std::atomic_flag外所有原子类型的另一个常见特性。

下一个最简单的原子类型是原子指针特化std::atomic<T*>,所以我们接下来将讨论它们。

5.2.4 std::atomic<T*>上的操作:指针运算

某种类型T的指针的原子形式是std::atomic<T*>,就像bool的原子形式是std::atomic<bool>。接口是相同的,尽管它操作的是对应指针类型的值,而不是bool值。与std::atomic<bool>一样,它既不是可拷贝构造的,也不是可拷贝赋值的,尽管它可以通过适当的指针值构造和赋值。除了必须的is_lock_free()成员函数外,std::atomic<T*>还有load(),store(),exchange(),compare_exchange_weak()和compare_exchange_strong()成员函数,其语义与std::atomic<bool>成员函数相似,同样获取和返回T*而不是bool。

提供给std::atomic<T*>的新操作为指针运算操作。基本操作由fetch_add()和fetch_sub()提供,它们在存储的地址上做原子加法和减法,以及+=, -=,前置后置的++和--提供简易的封装。操作符的工作方式与你在内置类型中预期的一样:如果x是指向Foo对象数组的第一个条目的std::atomic<Foo*>,那么x+=3将其更改为指向第四个条目,并返回一个普通的Foo*,该Foo*也指向第四个条目。fetch_add()和fetch_sub()稍微有点不同,它们返回初始值 (所以x.ftech_add(3)将更新x指向第四个元素,但返回指向数组第一个元素的地址)。这种操作也被称为交换并相加(exchange-and-add),并且这是一个原子的读-改-写操作,像exchange()和compare_exchange_weak()/compare_exchange_strong()一样。与其他操作一样,返回值是一个普通的T*值,而不是对std::atomic<T*>对象的引用,这样调用代码就可以根据之前的值执行操作:

class Foo{};
Foo some_array[5];
std::atomic<Foo*> p(some_array);
Foo* x=p.fetch_add(2);  // p加2,并返回老的值
assert(x==some_array);
assert(p.load()==&some_array[2]);
x=(p-=1);  // p减1,并返回新的值
assert(x==&some_array[1]);
assert(p.load()==&some_array[1]);

函数形式也允许内存顺序语义被指定为一个额外的函数调用参数:

p.fetch_add(3,std::memory_order_release);

因为fetch_add()和fetch_sub()都是读-改-写操作,可以拥有任意的内存顺序标签,并且可以加入到一个释放序列(release sequence)中。没法为操作符形式指定顺序语义,因为没办法提供必要的信息:因此这些形式都具有memory_order_seq_cst语义。

其余的基本原子类型都是相同的:它们都是原子整型,彼此具有相同的接口,只是相关的内置类型不同。我们将把他们看作一个群体。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值