C++并发:C++内存模型和原子操作
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); // 1y.store(true, std::memory_order_relaxed); // 2
}void read_y_then_x() {while (!y.load(std::memory_order_relaxed)); // 3if (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_flag | atomic<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,^= | √ | ||||
++,-- | √ | √ |
相关文章:

C++并发:C++内存模型和原子操作
C11引入了新的线程感知内存模型。内存模型精确定义了基础构建单元应当如何被运转。 1 内存模型基础 内存模型牵涉两个方面:基本结构和并发。 基本结构关系到整个程序在内存中的布局。 1.1 对象和内存区域 C的数据包括: 内建基本类型:int&…...

JavaScript函数中this的指向
总结:谁调用我,我就指向谁(es6箭头函数不算) 一、ES6之前 每一个函数内部都有一个关键字是 this ,可以直接使用 重点: 函数内部的 this 只和函数的调用方式有关系,和函数的定义方式没有关系 …...

【java学习笔记】@Autowired注解 使用方法和作用 | 配合@Component注解使用 | IOC控制反转
原本在类中,要用什么对象,就直接new一个对象。这种原始的方式 是由应用本身去控制实例的。 用了Autowired注解后,就相当于把实例(对象)的控制权 交给外部容器来统一管理(降低耦合)。(…...

数论问题76一一容斥原理
容斥原理是一种计数方法,用于计算多个集合的并集中元素的个数,以避免重复计算。以下是其基本内容及相关公式: 两个集合的容斥原理 若有集合A和集合B,那么A与B的并集中元素的个数等于A集合元素个数加上B集合元素个数,再…...

python-leetcode-从中序与后序遍历序列构造二叉树
106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode) # Definition for a binary tree node. # class TreeNode: # def __init__(self, val0, leftNone, rightNone): # self.val val # self.left left # self.right r…...

【Oracle篇】使用Hint对优化器的执行计划进行干预(含单表、多表、查询块、声明四大类Hint干预)
💫《博主介绍》:✨又是一天没白过,我是奈斯,从事IT领域✨ 💫《擅长领域》:✌️擅长阿里云AnalyticDB for MySQL(分布式数据仓库)、Oracle、MySQL、Linux、prometheus监控;并对SQLserver、NoSQL(…...

设置jmeter外观颜色
设置jmeter外观颜色 方法: 步骤一、点击顶部选项 ->外观,这里提供了不同的主题,可选自己喜欢的风格。 步骤二、选择后,弹框提示点击Yes。...

计算机网络 IP 网络层 2 (重置版)
IP的简介: IP 地址是互联网协议地址(Internet Protocol Address)的简称,是分配给连接到互联网的设备的唯一标识符,用于在网络中定位和通信。 IP编制的历史阶段: 1,分类的IP地址: …...

神经网络和深度学习
应用 类型 为什么近几年飞速发展 数据增长,算力增长,算法革新 逻辑回归 向量化 浅层神经网络(Shallow neural network) 单条训练数据前向传播计算表达式 batch训练数据前向传播计算表达式 反向传播计算表达式 参数随机初始化 不能全部设为0 原因是同一…...

MySQL 基础学习(3):排序查询和条件查询
MySQL 查询与条件操作:详解与技巧 在本文中,我们将探讨 MySQL 中的查询操作及其相关功能,包括别名、去重、排序查询和条件查询等,并总结一些最佳实践和注意事项。 一、使用别名(AS) 在查询中,…...

webAPI -DOM 相关知识点总结(非常细)
title: WebAPI语法 date: 2025-01-28 12:00:00 tags:- 前端 categories:- 前端WEB API 了解DOM的结构并掌握其基本的操作,体验 DOM 在开发中的作用 API简介 就是使用js来操作html和浏览器 什么是DOM? 就是一个文档对象模型,是用来呈现预计于任意htm…...

web集群
项目名称 基于keepalivednginx构建一个高可用、高性能的web集群 项目架构图 项目描述 构建一个基于nginx的7层负载均衡的web集群项目,模拟企业的业务环境达到构建一个高并发、高可用的web集群。通过压力测试来检验整个集群的性能,找出瓶颈࿰…...

Elasticsearch——Elasticsearch性能优化实战
摘要 本文主要介绍了 Elasticsearch 性能优化的实战方法,从硬件配置优化、索引优化设置、查询方面优化、数据结构优化以及集群架构设计等五个方面进行了详细阐述,旨在帮助读者提升 Elasticsearch 的性能表现。 1. 硬件配置优化 升级硬件设备配置一直都…...

不背单词快捷键(不背单词键盘快捷键)
文章目录 不背单词快捷键 不背单词快捷键 ᅟᅠ ᅟᅠ ᅟᅠ ᅟᅠ ᅟᅠ ᅟᅠ ᅟᅠ ᅟᅠ ᅟᅠ ᅟᅠ …...

kafka-保姆级配置说明(consumer)
bootstrap.servers #deserializer应该与producer保持对应 #key.deserializer #value.deserializer ##fetch请求返回时,至少获取的字节数,默认值为1 ##当数据量不足时,客户端请求将会阻塞 ##此值越大,客户端请求阻塞的时间越长&…...

1.五子棋对弈python解法——2024年省赛蓝桥杯真题
问题描述 原题传送门:1.五子棋对弈 - 蓝桥云课 "在五子棋的对弈中,友谊的小船说翻就翻?" 不!对小蓝和小桥来说,五子棋不仅是棋盘上的较量,更是心与心之间的沟通。这两位挚友秉承着"友谊第…...

python3+TensorFlow 2.x(三)手写数字识别
目录 代码实现 模型解析: 1、加载 MNIST 数据集: 2、数据预处理: 3、构建神经网络模型: 4、编译模型: 5、训练模型: 6、评估模型: 7、预测和可视化结果: 输出结果ÿ…...

杨辉三角(蓝桥杯2021年H)
输入一个数字,看杨辉三角压缩矩阵第几个数与之相等。 #include<iostream> using namespace std; /* typedef struct Node {int* data;int size;Node* next; }Node,*Linklist; */ int C(int a,int b) {//求解组合数int c 1,div 1;if (b 0) {c 1;}else {fo…...

【蓝桥杯嵌入式入门与进阶】2.与开发板之间破冰:初始开发板和原理图2
个人主页:Icomi 专栏地址:蓝桥杯嵌入式组入门与进阶 大家好,我是一颗米,本篇专栏旨在帮助大家从0开始入门蓝桥杯并且进阶,若对本系列文章感兴趣,欢迎订阅我的专栏,我将持续更新,祝你…...

C++ queue
队列用vector<int>好不好 不好 为什么? 因为队列是先进先出 vector没有提供头删(效率太低) 要强制适配也可以 就得用erase函数和begin函数了 库里面的队列是不支持vector<int>的 queue实现 #pragma once #include<vector…...

【MySQL-7】事务
目录 1. 整体学习思维导图 2. 什么是事务 2.1 事务的概念 2.2 事务的属性(ACID) 2.3 事务出现的原因 2.4 查看存储引擎对事务的支持 3. 事务的使用 3.1 事务的提交方式 3.1.1 手动提交 3.1.2 自动提交 结论: 3.2 事务的隔离级别 3.2.1 理解隔离 3.2.2…...

03链表+栈+队列(D1_链表(D1_基础学习))
目录 一、什么是链表 二、基本操作 三、为什么要使用链表 四、为什么能够在常数时间访问数组元素 数组优点 数组缺点 五、动态数组诞生 链表优点 链表缺点 六、链表、数组和动态数组的对比 七、 链表种类 1. 单向链表 2. 双向链表 3. 循环链表 八、链表衍生 ...…...

Git 出现 Please use your personal access token instead of the password 解决方法
目录 前言1. 问题所示2. 原理分析3. 解决方法前言 1. 问题所示 执行Git提交代码的时候,出现如下所示: lixiaosong@IT07 MINGW64 /f/java_project/JavaDemo (master) $ git push -u origin --all libpng warning: iCCP: known incorrect sRGB profile libpng warning...

AI大模型开发原理篇-1:语言模型雏形之N-Gram模型
N-Gram模型概念 N-Gram模型是一种基于统计的语言模型,用于预测文本中某个词语的出现概率。它通过分析一个词语序列中前面N-1个词的出现频率来预测下一个词的出现。具体来说,N-Gram模型通过将文本切分为长度为N的词序列来进行建模。 注意:这…...

STM32新建不同工程的方式
新建工程的方式 1. 安装开发工具 MDK5 / keil52. CMSIS 标准3. 新建工程3.1 寄存器版工程3.2 标准库版工程3.3 HAL/LL库版工程3.4 HAL库、LL库、标准库和寄存器对比3.5 库开发和寄存器的关系 4. STM32CubeMX工具的作用 1. 安装开发工具 MDK5 / keil5 MDK5 由两个部分组成&#…...

【Rust自学】14.5. cargo工作空间(Workspace)
喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(・ω・) 14.4.1. 为什么需要cargo workspace 假如说我们构建了一个二进制crate,里面既有library又有库。随着项目规模不断增长&#…...

全面了解 Web3 AIGC 和 AI Agent 的创新先锋 MelodAI
不管是在传统领域还是 Crypto,AI 都是公认的最有前景的赛道。随着数字内容需求的爆炸式增长和技术的快速迭代,Web3 AIGC(AI生成内容)和 AI Agent(人工智能代理)正成为两大关键赛道。 AIGC 通过 AI 技术生成…...

10.3 LangChain实战指南:解锁大模型应用的10大核心场景与架构设计
LangChain实战指南:解锁大模型应用的10大核心场景与架构设计 关键词: LangChain使用场景、LLM应用案例、检索增强生成、智能体开发、知识库问答 一、LangChain场景全景图:从简单到复杂的应用分层 #mermaid-svg-nzjpyXIPLzL0j3PG {font-family:"trebuchet ms",ver…...

Swing使用MVC模型架构
什么是MVC模式? MVC是一组英文的缩写,其全名是Model-View-Controller,也就是“模型-视图-控制器”这三个部分组成。这三个部分任意一个部分发生变化都会引起另外两个发生变化。三者之间的关系示意图如下所示: MVC分为三个部分,所以在MVC模型中将按照此三部分分成三…...

设计新的 Kibana 仪表板布局以支持可折叠部分等
作者:来自 Elastic Teresa Alvarez Soler, Hannah Mudge 及 Nathaniel Reese 在 Kibana 中构建可折叠仪表板部分需要彻底改造嵌入式系统并创建自定义布局引擎。这些更新改进了状态管理、层次结构和性能,同时为新的高级仪表板功能奠定了基础。 我们正在开…...