C++并发:C++内存模型和原子操作

news2025/1/30 7:53:54

C++11引入了新的线程感知内存模型。内存模型精确定义了基础构建单元应当如何被运转。

1 内存模型基础

内存模型牵涉两个方面:基本结构和并发。

基本结构关系到整个程序在内存中的布局。

1.1 对象和内存区域

C++的数据包括:

内建基本类型:int,float等

用户自定义类型:

不论对象属于什么类型,他都会存储在一个或多个内存区域之中,但是如果用到了位域,他有一项重要的性质:尽管相邻的位域分别属于不同对象,但照样算作同一内存区域。

首先,整个结构体是一个对象,由几个子对象构成,每个数据成员即为一个子对象。位域bf1和bf2共用一块内存区域,std::string对象s则由几块内存区域构成,别的数据成员都有各自的内存区域。

同时,bf3是0宽度位域(其变量名被注释掉,因为0宽度位域必须匿名),与bf4彻底分离,将bf4排除在bf3的内存区域之外,但实际bf3不占用任何内存

每个变量都是对象,对象的数据成员也是对象(基本类型这里也算在内了)

每个对象都至少占用一块内存区域

若变量属于内建基本类型,不论其大小,都占用一块内存区域,即便他们的位置相邻或他们是数列中的元素,相邻的位域属于同一内存区域

1.2 对象、内存区域和并发

所有与多线程相关的事项都会牵涉内存区域。若两个线程访问同一内存区域,并且有更新数据就要注意。

可以使用互斥或者原子操作同步。

因此:假设两个线程访问同一内存区域,却没有强制他们服从一定的访问次序,如果其中至少有一个是非原子化访问,并且至少有一个是写操作,就会出现数据竞争,导致未定义行为。

1.3 改动序列

在C++程序中,每个对象都具有一个改动序列,它由所有线程在对象上的全部写操作构成,其中第一个写操作即为对象的初始化,其中第一个写操作就是对象的初始化。

在程序的任意一次运行过程中,所含的全部线程都必须形成相同的改动序列。若多个线程共同操作某一对象,但是不属于原子类型,我们就要自己负责充分施行同步操作,确保对于一个变量,所有线程就其达成一致的改动序列。

不同的线程上观察属于同一个变量的序列,如果所见各异,就说明出现了数据竞争和未定义行为。若我们使用了原子操作,那么编译器有责任保证必要的同步操作有效。

为了实现上述保障,要求禁止某些预测执行,原因是:

在改动序列中,只要某线程看到过某个对象,则该线程的后续读操作必须获得相对新近的值,并且,该线程就同一对象后续写操作,必然出现在改动序列后方

如果某线程先向一个对象写数据过后再读取它,那么必须读取前面写的值。若在改动序列中,上述读写操作之间还有别的写操作,则必须读取最后写的值

2 C++中的原子操作及其类别

原子操作是不可分割的操作。非原子操作在完成一半时有可能为另一线程所见。

2.1 标准原子类型

标准原子类型定义在头文件<atomic>。

当然也可以使用互斥来模拟原子类型。is_lock_free可以判断给定类型上的操作是否能由原子指令直接实现。

原子操作的关键作用是替代需要互斥的同步方式,但是如果原子操作内使用了互斥,则可能无法达到所期望的性能提升,更好的做法是采用基于互斥的方式,更加直观且不易出错,比如无锁数据结构。

2.1.1 判定是否属于无锁结构

C++程序库专门为此提供了一组宏。针对由不同整数类型特化而成的各种原子类型,在编译器判定其是否属于无锁数据结构

C++17开始,全部原子类型都含有一个静态常量表达式成员变量,形如X::is_always_lock_free(不是全部即为false),功能与那些宏相同:考察编译期生成的一个特定版本的程序,当且仅当在所有支持改程序运行的硬件上,原子类型X全部由无锁结构形式实现。

上述宏分别是:

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

假设某原子类型从来不属于无锁结构,对应的宏取值为0

如果一直都是无锁结构,取值为2

如果等到运行时才能确定是否属于无锁结构(依赖硬件是否支持),取值为1.

只有一种原子类型不提供is_lock_free成员函数:std::atomic_flag

2.1.2 原子类型以及其别名

使用原子类型别名可以在不同编译器来替换成对应的std::atomic<>特化,或者该特化的基类。只要编译器完全支持C++17,它们唯一地表示对应的std::atomic<>特化。所以在同意程序内混用特化和别名会导致代码不可移植。

比如:

别名:atomic_bool

特化:std::atomic<bool>

以及标准原子类型的typedef对应标准库中内建类型的typedef

比如:

标准原子类型的typedef:atomic_int_least8_t

内建类型的typedef:int_least8_t

其余不一一列举,需要用到的时候直接查

2.1.3 原子类型操作以及内存次序语义

标准的原子类型对象无法复制,也无法赋值。但是它们可以接受内建类型的赋值,和隐式转换成内建类型。以及若干其他操作(exchange(),compare_exchange_weak(),compare_exchange_strong()等)

操作的类别决定了内存次序的准许取值,如果没有显式设定内存次序,则默认为最严格的std::memory_order_seq_cst

内存次序语义的枚举类std::memory_order由6个可能的值:

std::memory_order_relaxed,std::memory_order_acquire,std::memory_order_consume,std::memory_order_acq_rcl,std::memory_order_release,std::memory_order_seq_cst

原子操作被划分为如下三类:

原子操作类别可选用的内存次序
存储(store)操作std::memory_order_relaxed
std::memory_order_release
std::memory_order_seq_cst
载入(load)操作std::memory_order_relaxed
std::memory_order_consume
std::memory_order_acquire
std::memory_order_seq_cst
读-写-改(read-modify-write)操作std::memory_order_relaxed
std::memory_order_consume
std::memory_order_acquire
std::memory_order_release
std::memory_order_acq_rel
std::memory_order_seq_cst

2.2 操作std::atomic_flag

std::atomic_flag是最简单的标准原子类型,表示一个布尔标志。唯一地用途是充当构建单元,因此我们认为普通开发者一般不会直接使用他。

std::atomic_flag类型的对象必须由宏ATOMIC_FLAG_INIT初始化,它把标志初始化为置零状态:

std::atomic_flag f = ATOMIC_FLAG_INIT(只能初始化为置零状态)

它是唯一保证无锁的原子类型,如果std::atomic_flag具有静态存储期,它就会保证以静态方式初始化,从而避免初始化次序的问题。

完成std::atomic_flag对象的初始化后,我们只能执行3种操作:销毁置零读取原有的值并设置标志成立。这分别对应于析构函数,成员函数clear(),成员函数test_and_set()。

我们可以为clear(),test_and_set()指定内存次序。

clear()是存储操作,无法采用std::memory_order_acquire和std::memory_order_acq_rel。

test_and_set()是 读-写-改(read-modify-write)操作 因此能使用任何次序。

2.2.2 为什么原子对象不能复制和赋值

按定义,原子类型上的操作都是原子化的,但拷贝赋值和拷贝构造都涉及两个对象,而牵涉两个不同对象的单一操作却是无法原子化的。在拷贝构造或拷贝赋值的过程中,必须先从来源对象读取值,再将其写出到目标对象。这是在两个独立对象的两个独立操作,其组合不可能是原子化的。

因为std::atomic_flag功能有限,所以它可以完美扩展成自旋转互斥。最开始原子标志置零,表示互斥没有加锁。我们反复调用test_and_set()试着锁住互斥,一旦读取的值变为false,则说明线程已将标志设置成立,循环终止。简单地将标志置零即可解除互斥。

#include <atomic>

class spinlock_mutex {
    std::atomic_flag flag;

public:
    spinlock_mutex() : flag(ATOMIC_FLAG_INIT) {}

    void lock() {
        while (flag.test_and_set(std::memory_order_acquire));
    }
    void unlock() {
        flag.clear(std::memory_order_release);
    }
};

上述实现已经能配合lock_guard<>运用自如。

但是std::atomic_flag受限严格,不支持单纯的无修改查值操作,因此最好使用std::atomic<bool>

2.3 操作std::atomic<bool>

原子类型的赋值操作不返回当前值的引用,而是按值返回。(返回引用有可能导致在执行原子操作时引入竞争)

通过调用store(),能够设定内存次序语义。(存储操作)

提供了更通用的exchange()来代替test_and_set(),它获取原有的值,让我们自行选定新值作为替换。(读-改-写)

还支持单纯的读取(没有伴随修改行为):隐式做法是将实例转为普通布尔值,显式做法是调用load()。(载入操作)

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

2.3.1  比较-交换 操作

其实现形式是成员函数compare_exchange_weak()和compare_exchange_strong()。

比较交换操作原子类型编程的基石。使用者给定一个期望值,原子变量将他和自身比较如果相等,就存入另一既定的值;否则更新期望值所需变量,向它赋予原子变量的值。如果完成了保存操作(两值相等),操作成功,返回true;操作失败返回false

2.3.2 compare_exchange_weak()

对于compare_exchange_weak(),即使原子变量等于期望值,保存动作仍然有可能失败,这种情形下,原子变量维持原值不变,compare_exchange_weak()返回false。

佯败:原子化的比较-交换必须要由一条指令单独完成,而某些处理器没有这种指令,无从保证该操作按原子化方式完成。要实现比较-交换,负责的线程需要改为连续运行一系列指令,但在这些计算机上,只要出现线程数量多余处理器数量的情形,线程就有可能执行到中途因系统调度而切出,导致失败。这种保存失败叫佯败。

compare_exchange_weak()可能佯败,因此经常配合循环使用:

    bool expected = false;
    extern std::atomic<bool> b;
    while (!b.compare_exchange_weak(expected, true) && !expected);

2.3.3 compare_exchange_strong()

compare_exchange_strong()在原子变量的值不符合预期时返回false。这让我们能够了解是否修改成功或者是否存在零一线程抢先切入导致佯败。

相较于compare_exchange_weak(),compare_exchange_strong()会形成双重循环(其自身内部含有一个循环),不利于性能;反之,如果存入的值需要耗时的计算,选用compare_exchange_strong()更加合理,因为只要预期值没变化就可以避免重复计算

2.3.4 比较-交换 操作

其还有一个特殊之处,它们接收两个内存次序参数。使得程序能区分成功和失败两种情况,采用不同的内存次序语义。

如:合适的做法是:

操作成功,使用std::memory_order_acq_rel

操作失败,使用std::memory_order_relaxed

失败操作没有存储行为,所以不可能采用std::memory_order_acq_rel和std::memory_order_release。因此这两种内存次序不被允许作为失败操作的参数。失败操作内存次序不能比成功操作更严格。

若要将失败操作的内存次序设定为:std::memory_order_acquire和std::memory_order_seq_cst,则成功操作要设定同样的内存次序。如果没有为失败操作设定内存次序,那么编译器就假定它和成功操作有同样的内存次序,但其中的释放语义会被移除。若都没设定,则使用默认次序:std::memory_order_seq_cst。

2.4 操作std::atomic<T*>:算术形式的指针运算

指向类型T的指针的原子化形式。

也具有is_lock_free,load,store,exchange,compare_exchange_weak和compare_exchange_strong。

除此之外还提供了新的操作:算术形式的指针运算。

fetch_add()和fetch_sub(),分别对对象中存储的地址进行原子化的加减。并且也重载了:++,--,+=,-=运算符。(fetch_add,fetch_sub返回运算前的地址),也是一种 读-改-写 操作。

#include <atomic>

class Foo{};
Foo some_array[5];
std::atomic<Foo*> p(some_array);
Foo* x = p.fetch_add(2);
assert(x==some_array);
assert(p.load()==&some_array[2]);
x=(p-=1);
assert(x==&some_array[1]);
assert(p.load()==&some_array[1]);

x=(p-=1);令p减1,返回新值。

2.5 操作标准整数原子类型

比如std::atomic<int>等,可以执行很多操作。

load,store,exchange,compare_exchange_weak和compare_exchange_strong。

以及原子运算(fetch_add(),fetch_sub(),fetch_or()和fetch_xor())以及这些运算的复合赋值形式(+=,-=,&=,|=,^=)和前后缀形式的自增和自减。

但是没有乘除和位移的重载。

2.6 泛化的std::atomic<>类模板

对于某个自定义类型UDT,要满足一定条件才能实现std::atomic<UDT>:

必须具备平实拷贝赋值操作符(平直,简单的原始内存复制及其等效操作),它不得含有任何虚函数,也不可以从虚基类派生而出,还必须由编译器代其隐式生成拷贝赋值操作符。

另外,若自定义类型具有基类或非静态数据成员,则他们必须具备平实拷贝赋值运算符。

由于以上限制,赋值操作不涉及任何用户编写的代码,编译器可借用memcpy()或采取与之等效的行为完成它。

比较-交换操作采用的是逐位比较运算,效果等同于直接使用memcmp(),因此若自定义类型含有填充位,却不参与普通比较操作,那么即使UDT对象值相等,比较-交换操作还是会失败。

对std::atomic<float>和std::atomic<double>调用compare_exchange_strong()需要注意,浮点值的算术原子运算并不存在。假设我们用一个这一特化调用compare_exchange_strong(),会因为是80.0或者5*(2^4)或者20*(2^2)这样的表示方式不同被判定值不一样

2.7 原子操作的非成员函数

还有众多非成员函数,与各原子操作逐一等价。

大部分冠以前缀“atomic_”

只要有可能指定内存次序,就演化出两个变体:

带有后缀“_explicit”,接收更多参数以指定内存次序

不带有后缀也不接收内存次序参数

如:

std::atomic_store_explicit(&atomic_var, new_value, std::memory_order_release)

std::atomic_store(&atomic_var, new_value);

3 同步操作和强制次序

3.1 同步关系

同步关系只存在于原子类型的操作之间。如果一种数据结构含有原子类型,并且其整体操作都涉及恰当的内部原子操作,那么该数据结构之间的多次操作(如锁定互斥)就可能存在同步关系。但同步关系从根本上说来自原子类型的操作。

同步关系的基本思想是:对变量x执行原子写操作W原子读操作R,且两者都有恰当的标记(原子类型上全部的操作都默认添加适当的标记,也就是在C++内存模型中,操作原子类型时所受的各种次序约束)。只要满足下面其中一点,它们即彼此同步:

1 R读取W直接存入的值

2 W所属线程随后还执行了另一原子写操作,R读取了后面存入的值

3 任意线程执行一连串“读-写-改”操作(如fetch_add()或compare_exchange_weak()),而其中第一个操作读取的值由W写出。

3.2 先行关系

先行关系和严格先行关系是在程序中确立操作次序的基本要素:它们的用途是清楚界定哪些操作能看见其他哪些操作产生的结果

单一线程内,这种关系通常非常直观:若某项操作按控制流程顺序在另一项之前执行,前者即先于后者发生,且前者严格先于后者发生。

如果甲,乙操作分别由不同线程执行,且它们同步,则甲操作跨线程地先于乙操作发生。这也是可传递的关系:甲先于乙,乙先于丙,则甲先于丙。

线程间先行关系先行关系中,各种操作都被标记为memory_order_consume,而严格先行关系则无此标记。

3.3 原子操作的内存次序

6种内存次序代表3种模式:

先后一致次序std::memory_order_seq_cst
获取-释放次序std::memory_order_consume,std::memory_order_acq_rel,std::memory_order_acquire,std::memory_order_release
宽松次序std::memory_order_relaxed

在不同的CPU架构上(ARM,X86),这几种内存模型也许会有不同的运行开销。

3.3.1 先后一致次序

默认内存次序为“先后一致次序”,如果程序服从该次序,就简单地把一切事件视为按先后顺序发生,其操作与这种次序保持一致。

假设在多线程程序的全部原子类型的实例上,所有操作都保持先后一致,那么若将他们按某种特定次序改由单线程程序执行,则两个程序的操作将毫无区别。

弱保护的多处理计算机上,保序操作会导致严重的性能损失,因为他必须在多处理器之间维持全局操作次序,而这很可能要在处理器之间进行大量同步操作,代价高昂,所以某些处理器架构(如x86)提供了相对低廉的方式以维持先后一致。

#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x, y;
std::atomic<int> z;

void write_x() {
    x.store(true, std::memory_order_seq_cst);
}

void write_y() {
    y.store(true, std::memory_order_seq_cst);
}

void read_x_then_y() {
    while (!x.load(std::memory_order_seq_cst));
    if (y.load(std::memory_order_seq_cst)) {
        ++z;
    }
}

void read_y_then_x() {
    while (!y.load(std::memory_order_seq_cst));
    if (x.load(std::memory_order_seq_cst)) {
        ++z;
    }
}

int main() {
    x = false;
    y = false;
    z = 0;
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join();
    b.join();
    c.join();
    d.join();
    assert(z.load()!=0);
}

x和y其中的一个存储操作必然先行发生,因为有wile循环拦截。按照memory_order_seq_cst次序,所有以他为标记的操作形成单一的全局总操作序列,因此变量y的载入操作和存储操作会构成某种次序关系。

先后一致次序最直观,最符合直觉,但要求在所有线程间进行全局同步,因此也是代价最高的内存次序。

3.3.2 非先后一致次序

多个线程不必就事件发生次序达成一致。它仅要求一点:全部线程在每个独立变量上都达成一致的修改序列。不同变量上的操作构成其特有序列,假设各种操作都受施加的内存次序约束,若线程都能看到变量的值相应地保持一致,就容许这个操作序列在各线程中出现差别。

3.3.3 宽松次序

如果采用宽松次序,那么原子类型上的操作不存在同步关系。在单一线程内,同一个变量上的操作仍然服从先行关系,但几乎不要求线程间存在任何次序关系。该内存次序的唯一要求是,在一个线程内,对相同变量的访问次序不得重新编排。

对于给定的线程,一旦它见到某原子变量在某时刻持有的值,则该线程的后续读操作不可能读取相对更早的值。

#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y() {
    x.store(true, std::memory_order_relaxed);   // 1
    y.store(true, std::memory_order_relaxed);   // 2
}

void read_y_then_x() {
    while (!y.load(std::memory_order_relaxed));   // 3
    if (x.load(std::memory_order_relaxed)) {      // 4
        ++z;
    }
}

int main() {
    x = false;
    y = false;
    z = 0;
    std::thread a(write_x_then_y);
    std::thread b(read_y_then_x);
    a.join();
    b.join();
    assert(z.load()!=0);                            // 5
}

使用宽松次序后,执行示意图如下:

3.3.4 理解宽松次序

除非万不得已,强烈建议避免使用宽松次序,仅仅牵涉两个线程和原子变量,代码产生的结果就会和预期不符合。可以使用 获取-释放 次序,它避免了 绝对先后一致次序 的额外开销。

3.3.5 获取-释放次序

比宽松次序严格一些,会产生一定的同步效果,而不会形成服从先后一致次序的全局总操作序列。在该内存模型中,原子化载入即为获取操作(memory_order_acquire),原子化存储即为释放操作(memory_order_release),而原子化“读-改-写”操作(比如fetch_add()和exchange())则为获取或释放操作,或二者皆是(memory_order_acq_rel)。

换而言之,若多个线程服从获取-释放次序,则其所见的操作序列可能各异,但其差异的程度和方式都受到一定条件的制约。

#include <thread>
#include <atomic>
#include <assert.h>

std::atomic<bool> x, y;
std::atomic<int> z;

void write_x_then_y() {
    x.store(true, std::memory_order_relaxed);
    y.store(true, std::memory_order_release);
}

void read_y_then_x() {
    while (!y.load(std::memory_order_acquire));
    if (x.load(std::memory_order_relaxed)) {      // 2
        ++z;
    }
}

int main() {
    x = false;
    y = false;

    z = 0;
    std::thread c(write_x_then_y);
    std::thread d(read_y_then_x);

    c.join();
    d.join();

    assert(z.load());
}

这里,可以通过y的store和load之间设定memory_order_release和memory_order_acquire,来保证y的写入和读取服从次序,进而限制了write_x_then_y在read_y_then_x之前。

3.3.6 通过 获取-释放 次序传递同步

如果我们使用“读-改-写”操作,选择满足的内存次序语义是关键。上面的场景中,我们同时需要获取语义和释放语义,所以选择memory_order_acq_rel正合适。

因为,采用memory_order_acquire次序的fetch_sub()不会与任何操作同步,因为他不是释放操作。类似的,存储操作无法与采用memory_order_release次序的fetch_or()同步,因为fetch_or()不是获取操作。

假设使用获取-释放次序实现简单的锁,那么考察一份使用该锁的代码,其行为表现将服从先后一致次序,而加锁和解锁之间的内部行为则不一定。(因为锁的使用限制了指令重排,加锁之前的代码不会被重新编排到其后面,解锁之后的代码也不会被重新编排到其前面)

如果原子操作对先后一致的要求不是很严格,那么由成对的获取-释放操作实现同步,开销会远低于由保序操作实现的全局一致顺序。这种做法很费脑力,要求周密思量线程间那些违背一般情况的行为,从而保证不会出错,让程序服从施加的次序正确运行。

3.3.7 获取-释放 次序和memory_order_consume次序造成的数据依赖

memory_order_consume次序是获取释放次序的组成部分,但是它完全针对数据依赖,引入了线程间先行关系中的数据依赖细节,C++17标准建议不予采用。因此我们不应在代码中使用memory_order_consume。

数据依赖:第一项得出的结果由第二项继续处理,即构成数据依赖。数据依赖需要处理两种关系:前序依赖(可以存在于线程之间),携带依赖(单一线程中的内部关系)。

若代码中有大量携带依赖,会有额外开销,可以使用std::kill_dependency打断依赖链。

在实际的代码中,凡是要用到memory_order_consume次序的情形,我们应当一律改成用memory_order_acquire次序,而使用std::kill_dependency()是没有必要的。

3.4 释放序列和同步关系

针对同一个原子变量,我们可以在不同的线程对其进行存储和载入操作,从而构成同步关系。即使存储和读取之间还另外存在多个“读-写-改”操作,同步关系仍然成立,但这一切的前提条件是,所有操作都采用合适的内存次序。

如果存储操作是:

memory_order_release,memory_order_acq_rel,memory_order_seq_cst

载入操作是:

memory_order_consume,memory_order_acquire,memory_order_seq_cst

这些操作前后相扣成链,每次载入的值都源自前面的存储操作,那么该操作链由一个释放序列组成。如果最后的载入操作服从内存次序memory_order_acquire,memory_order_seq_cst,那么最初的存储操作与它构成同步关系。如果最后的载入操作服从内存次序memory_order_consume,那么两者构成前序依赖关系。

#include <atomic>
#include <thread>
#include <vector>
std::vector<int> queue_data;
std::atomic<int> count;

void populate_queue() {
    unsigned const number_of_items = 20;
    queue_data.clear();
    for(unsigned i = 0; i < number_of_items; ++i) {
        queue_data.push_back(i);
    }
    count.store(number_of_items, std::memory_order_release);
}

void consume_queue_items() {
    while (true) {
        int item_index;
        if ((item_index = count.fetch_sub(1, std::memory_order_acquire)) <= 0) {
            wait_for_more_items();
            continue;
        }
        process(queue_data[item_index-1]);
    }
}

int main() {
    std::thread a(populate_queue);
    std::thread b(consume_queue_items);
    std::thread c(consume_queue_items);

    a.join();
    b.join();
    c.join();
}

3.5 栅栏

用途是强制施加内存次序,通常与服从memory_order_relaxed次序的原子操作组合使用。栅栏操作全部通过全局函数执行。当线程运行至栅栏处时,它便对线程中其他原子操作的次序产生作用。

#include <thread>
#include <atomic>
#include <assert.h>

std::atomic<bool> x, y;
std::atomic<int> z;

void write_x_then_y() {
    x.store(true, std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_release);
    y.store(true, std::memory_order_release);
}

void read_y_then_x() {
    while (!y.load(std::memory_order_relaxed));
    std::atomic_thread_fence(std::memory_order_acquire);
    if (x.load(std::memory_order_relaxed)) {      // 2
        ++z;
    }
}

int main() {
    x = false;
    y = false;

    z = 0;
    std::thread c(write_x_then_y);
    std::thread d(read_y_then_x);

    c.join();
    d.join();

    assert(z.load());
}

3.6 凭借原子操作让非原子操作服从内存次序

3.7 强制非原子操作服从内存次序

3.7.1 std::thread

1 构造std::thread实例的线程和传入std::thread构造函数的可调用对象构成同步

2 若负责管控线程的std::thread对象上执行了join调用,并且此函数成功返回,则该线程的运行完成与这一返回动作同步。

3.7.2 std::mutex,std::timed_mutex,std::recursive_mutex,std::recursive_timed_mutex

1 给定一互斥对象,其上的lock()和unlock()的全部调用,以及try_lock(),try_lock_for()和try_lock_until()的成功调用会形成单一总序列,即对该互斥进行加锁和解锁的操作序列。

2 给定一互斥对象,在其加锁和解锁的操作序列,每个unlock()调用都与下一个lock()调用同步,或与下一个try_lock(),try_lock_for(),try_lock_until()的成功调用同步(失败则不为同步)。

3.7.3 std::shared_mutex和std::shared_timed_mutex

1 给定一个互斥对象,其上的lock(),unlock(),lock_shared()和unlock_shared()的全部调用,以及try_lock(),try_lock_for(),try_lock_until(),try_lock_shared(),try_lock_shared_for(),try_lock_shared_until()的成功调用会形成单一总序列,即对该互斥进行加锁和解锁的操作序列。

2 给定一互斥对象,在其加锁和解锁的操作序列,每个unlock()调用都与下一个lock()调用同步,或与下一个try_lock(),try_lock_for(),try_lock_until(),try_lock_shared(),try_lock_shared_for(),try_lock_shared_until()的成功调用同步(失败则不为同步)。

3.7.4 std::promise,std::future,shd::shared_future

1 给定一个std::promise对象,则我们由get_future()得到的关联的std::future对象,他们共享异步状态。如果std::promise上的set_value()或set_exception()调用成功,又如果我们接着在该std::future对象上调用wait(),get(),wait_for()或wait_until(),成功返回std::future_statue::ready,那么这两次调用的成功返回构成同步。

2 给定一个std::promise对象,则我们由get_future()得到的关联的std::future对象,他们共享异步状态。如果出现异常,该异步状态会存储一个std::future_error异常对象,又如果我们在关联的std::future对象上调用wait(),get(),wait_for()或wait_until(),成功返回std::future_status::ready,那么std::promise对象的析构函数与该成功返回构成同步。

3.7.5 std::package_task,std::future,std::shared_future

1 给定一个std::package_task对象,则我们由get_future()得到的关联的std::future对象,他们共享异步状态。若包装的任务由std::package_task的函数调用操作符运行,我们在关联的std::future对象上调用wait(),get(),wait_for()或wait_until(),成功返回std::future_status::ready,那么任务的运行完结该成功调用返回构成同步。

2 给定一个std::package_task对象,则我们由get_future()得到的关联的std::future对象,他们共享异步状态。如果出现异常,该异步状态会存储一个std::future_error异常对象,又如果我们在关联的std::future对象上调用wait(),get(),wait_for()或wait_until(),成功返回std::future_status::ready,那么std::package_task对象的析构函数的运行与该成功返回构成同步。

3.7.6 std::async,std::future,std::shared_future

1 如果一项任务通过调用std::async而启动,以std::launch::async方式在其他线程上异步地运行,则该std::async调用会生成一个关联的std::future对象,它与启动的任务共享异步状态。若我们在该std::future对象上调用wait(),get(),wait_for()或wait_until(),成功返回std::future_status::ready,那么任务的线程运行完结该成功调用返回构成同步。

2 如果一项任务通过调用std::defer而启动,以std::launch::deferred方式在当前线程上同步地运行,则该std::async调用会生成一个关联的std::future对象,它与启动的任务共享异步状态。若我们在该std::future对象上调用wait(),get(),wait_for()或wait_until(),成功返回std::future_status::ready,那么任务的运行完结该成功调用返回构成同步。

3.7.7 std::experimental::future,std::experimental::shared_future和后续函数

3.7.8 std::experimental::latch

3.7.9 std::experimental::barrier

3.7.10 std::experimental::flex_barrier

3.7.11 std::condition_variable和std::condition_variable_any

4 小结

各原子类型上可执行的操作

操作atomic_flagatomic<bool>atomic<T*>整数原子类型其他原子类型
test_and_set
clear
is_lock_free
load
store
exchange

compare_exchange_weak

compare_exchange_strong

fetch_add,+=
fetch_sub,-=
fetch_or,|=
fetch_and,&=
fetch_xor,^=
++,--

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2286336.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

宝塔mysql数据库容量限制_宝塔数据库mysql-bin.000001占用磁盘空间过大

磁盘空间占用过多&#xff0c;排查后发现网站/www/wwwroot只占用7G&#xff0c;/www/server占用却高达8G&#xff0c;再深入排查发现/www/server/data目录下的mysql-bin.000001和mysql-bin.000002两个日志文件占去了1.5G空间。 百度后学到以下知识&#xff0c;做个记录。 mysql…

2859.计算K置位下标对应元素的和

示例 1&#xff1a;输入&#xff1a;nums [5,10,1,5,2], k 1 输出&#xff1a;13 解释&#xff1a;下标的二进制表示是&#xff1a; 0 0002 1 0012 2 0102 3 0112 4 1002 下标 1、2 和 4 在其二进制表示中都存在 k 1 个置位。 因此&#xff0c;答案为 nums[1] nums[…

8. 网络编程

网络的基本概念 TCP/IP协议概述 OSI和TCP/IP模型 socket&#xff08;套接字&#xff09; 创建socket 字节序 字节序转换函数 通用地址结构 因特网地址结构 IPV4地址族和字符地址间的转换(点分十进制->网络字节序) 填写IPV4地址族结构案例 掌握TCP协议网络基础编程 相关函数 …

关于opencv环境搭建问题:由于找不到opencv_worldXXX.dll,无法执行代码,重新安装程序可能会解决此问题

方法一&#xff1a;利用复制黏贴方法 打开opencv文件夹目录找到\opencv\build\x64\vc15\bin 复制该目录下所有文件&#xff0c;找到C:\Windows\System32文件夹&#xff08;注意一定是C盘&#xff09;黏贴至该文件夹重新打开VS。 方法二&#xff1a;直接配置环境 打开opencv文…

Git Bash 配置 zsh

博客食用更佳 博客链接 安装 zsh 安装 Zsh 安装 Oh-my-zsh github仓库 sh -c "$(curl -fsSL https://install.ohmyz.sh/)"让 zsh 成为 git bash 默认终端 vi ~/.bashrc写入&#xff1a; if [ -t 1 ]; thenexec zsh fisource ~/.bashrc再重启即可。 更换主题 …

DeepSeek-R1 本地部署模型流程

DeepSeek-R1 本地部署模型流程 ***************************************************** 环境准备 操作系统&#xff1a;Windows11 内存&#xff1a;32GB RAM 存储&#xff1a;预留 300GB 可用空间 显存: 16G 网络: 100M带宽 ********************************************…

C++ unordered_map和unordered_set的使用,哈希表的实现

文章目录 unordered_map&#xff0c;unorder_set和map &#xff0c;set的差异哈希表的实现概念直接定址法哈希冲突哈希冲突举个例子 负载因子将关键字转为整数哈希函数除法散列法/除留余数法 哈希冲突的解决方法开放定址法线性探测二次探测 开放定址法代码实现 哈希表的代码 un…

C#通过3E帧SLMP/MC协议读写三菱FX5U/Q系列PLC数据案例

C#通过3E帧SLMP/MC协议读写三菱FX5U/Q系列PLC数据案例&#xff0c;仅做数据读写报文测试。附带自己整理的SLMP/MC通讯协议表。 SLMP以太网读写PLC数据20191206/.vs/WindowsFormsApp7/v15/.suo , 73216 SLMP以太网读写PLC数据20191206/SLMP与MC协议3E帧通讯协议表.xlsx , 10382…

Unity|小游戏复刻|见缝插针1(C#)

准备 创建Scenes场景&#xff0c;Scripts脚本&#xff0c;Prefabs预制体文件夹 修改背景颜色 选中Main Camera 找到背景 选择颜色&#xff0c;一种白中透黄的颜色 创建小球 将文件夹里的Circle拖入层级里 选中Circle&#xff0c;位置为左右居中&#xff0c;偏上&…

数据结构的队列

一.队列 1.队列&#xff08;Queue&#xff09;的概念就是先进先出。 2.队列的用法&#xff0c;红色框和绿色框为两组&#xff0c;offer为插入元素&#xff0c;poll为删除元素&#xff0c;peek为查看元素红色的也是一样的。 3.LinkedList实现了Deque的接口&#xff0c;Deque又…

HTML-新浪新闻-实现标题-排版

标题排版 图片标签&#xff1a;<img> src&#xff1a;指定图片的url&#xff08;绝对路径/相对路径&#xff09; width&#xff1a;图片的宽度&#xff08;像素/相对于父元素的百分比&#xff09; heigth&#xff1a;图片的高度&#xff08;像素/相对于父元素的百分比&a…

C语言二级题解:查找字母以及其他字符个数、数字字符串转双精度值、二维数组上下三角区域数据对调

目录 一、程序填空题 --- 查找字母以及其他字符个数 题目 分析 二、程序修改 --- 数字字符串转双精度值 题目 分析 小数位字符串转数字 三、程序设计 --- 二维数组上下三角区域数据对调 题目 分析 前言 本文来讲解&#xff1a; 查找字母以及其他字符个数、数字字符串…

VPR概述、资源

SOTA网站&#xff1a; Visual Place Recognition | Papers With Code VPR&#xff08;Visual Place Recognition&#xff09; 是计算机视觉领域的一项关键任务&#xff0c;旨在通过图像匹配和分析来识别场景或位置。它的目标是根据视觉信息判断某个场景是否与数据库中的场景匹…

Electron学习笔记,安装环境(1)

1、支持win7的Electron 的版本是18&#xff0c;这里node.js用的是14版本&#xff08;node-v14.21.3-x86.msi&#xff09;云盘有安装包 Electron 18.x (截至2023年仍在维护中): Chromium: 96 Node.js: 14.17.0 2、安装node环境&#xff0c;node-v14.21.3-x86.msi双击运行选择安…

58.界面参数传递给Command C#例子 WPF例子

界面参数的传递&#xff0c;界面参数是如何从前台传送到后台的。 param 参数是从界面传递到命令的。这个过程通常涉及以下几个步骤&#xff1a; 数据绑定&#xff1a;界面元素&#xff08;如按钮&#xff09;的 Command 属性绑定到视图模型中的 RelayCommand 实例。同时&#x…

Git图形化工具【lazygit】

简要介绍一下偶然发现的Git图形化工具——「lazygit」 概述 Lazygit 是一个用 Go 语言编写的 Git 命令行界面&#xff08;TUI&#xff09;工具&#xff0c;它让 Git 操作变得更加直观和高效。 Github地址&#xff1a;https://github.com/jesseduffield/lazygit 主要特点 主要…

三个不推荐使用的线程池

线程池的种类 其实看似这么多的线程池&#xff0c;都离不开ThreadPoolExecutor去创建&#xff0c;只不过他们是简化一些参数 newFixedThreadPool 里面全是核心线程 有资源耗尽的风险&#xff0c;任务队列最大长度为Integer.MAX_VALUE&#xff0c;可能会堆积大量的请求&#xff…

星际战争模拟系统:新月的编程之道

星际战争模拟系统&#xff1a;新月的编程之道 作为一名在 25 世纪星际时代成长起来的科学家和军事战略家&#xff0c;我对编程和人工智能的热爱始于童年。我的父亲是一位著名的物理学家&#xff0c;母亲是一位杰出的生物工程师。在他们的影响下&#xff0c;我从小就对科学和技术…

【CS61A 2024秋】Python入门课,全过程记录P4(Week7 Generators开始,更新于2025/1/29)

文章目录 关于基本介绍&#x1f44b;新的问题更好的解决方案Week7Mon Generators阅读材料Lab 05: Iterators, MutabilityQ1: WWPD: List-MutationQ2: Insert Items 关于 个人博客&#xff0c;里面偶尔更新&#xff0c;最近比较忙。发一些总结的帖子和思考。 江湖有缘相见&…

Fort Firewall:全方位守护网络安全

Fort Firewall是一款专为 Windows 操作系统设计的开源防火墙工具&#xff0c;旨在为用户提供全面的网络安全保护。它基于 Windows 过滤平台&#xff08;WFP&#xff09;&#xff0c;能够与系统无缝集成&#xff0c;确保高效的网络流量管理和安全防护。该软件支持实时监控网络流…