《C++并发编程实战》读书笔记(4):原子变量
1、标准原子类型
标准原子类型的定义位于头文件<atomic>内。原子操作的关键用途是取代需要互斥的同步方式,但假设原子操作本身也在内部使用了互斥,就很可能无法达到期望的性能提升。有三种方法来判断一个原子类型是否属于无锁数据结构:
- 所有标准原子类型(
std::atomic_flag除外,因为它必须采取无锁操作)都具有成员函数is_lock_free(),若它返回true则表示给定类型上的操作是能由原子指令直接实现的,若返回false则表示需要借助编译器和程序库的内部锁来实现。 - C++程序库提供了一组宏:
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表示对应的std::atomic<>特化类型从来都不属于无锁结构,取值为1表示运行时才能确定是否属于无锁结构,取值为2表示它一直属于无锁结构。 - 从C++17开始,全部原子类型都含有一个静态常量表达式成员变量
X::is_always_lock_free,功能与上述那些宏相同,用于在编译期判定一个原子类型是否属于无锁结构。当且仅当在所有支持运行该程序的硬件上,原子类型X全都以无锁结构形式实现,该成员变量的值才为true。
除了std::atomic_flag,其余原子类型都是通过模板std::atomic<>特化得到的。由内建类型特化得到的原子类型,其接口反映出自身性质,例如C++标准没有为普通指针定义位运算(如&=),所以不存在专为原子化指针而定义的位运算。一些内建类型的std::atomic<>特化如下表:
| 原子类型的别名 | 对应的特化 |
|---|---|
| atomic_bool | std::atomic<bool> |
| atomic_char | std::atomic<char> |
| atomic_schar | std::atomic<signed char> |
| atomic_uchar | std::atomic<unsigned char> |
| atomic_int | std::atomic<int> |
| atomic_uint | std::atomic<unsigned> |
| atomic_short | std::atomic<short> |
| atomic_ushort | std::atomic<unsigned short> |
| atomic_long | std::atomic<long> |
| atomic_ulong | std::atomic<unsigned long> |
| atomic_llong | std::atomic<long long> |
| atomic_ullong | std::atomic<unsigned long long> |
| atomic_char16_t | std::atomic<char16_t> |
| atomic_char32_t | std::atomic<char32_t> |
| atomic_wchar_t | std::atomic<wchar_t> |
原子类型对象无法复制,也无法赋值,但可以接受内建类型赋值,也支持隐式地转换成内建类型。需要注意的是:按照C++惯例,赋值操作符通常返回一个引用,指向接受赋值的目标对象;而原子类型的赋值操作符不返回引用,而是按值返回(该值属于对应的非原子类型)。
2、原子操作
各种原子类型上可以执行的操作如下表所示:
| 操作 | atomic_flag | atomic<bool> | atomic<T*> | 整数原子类型 | 其它原子类型 |
|---|---|---|---|---|---|
| test_and_set | Y | ||||
| clear | Y | ||||
| is_lock_free | Y | Y | Y | Y | |
| load | Y | Y | Y | Y | |
| store | Y | Y | Y | Y | |
| exchange | Y | Y | Y | Y | |
| compare_exchange_weak, compare_exchange_strong | Y | Y | Y | Y | |
| fetch_add, += | Y | Y | |||
| fetch_sub, -= | Y | Y | |||
| fetch_or, |= | Y | ||||
| fetch_and, &= | Y | ||||
| fetch_xor, ^= | Y | ||||
| ++, -- | Y | Y |
2.1、操作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有两个成员函数:
clear():将标志清零。test_and_set():获取旧值并设置标志成立。
使用std::atomic_flag实现一个自旋锁的示例如下:
class spinlock_mutex
{
std::atomic_flag flag;
public:
spinlock_mutex() : flag(ATOMIC_FLAG_INIT) {}
void lock()
{
while (flag.test_and_set());
}
void unlock()
{
flag.clear();
}
};
2.2、操作std::atomic<bool>
相比于std::atomic_flag,std::atomic<bool>是一个功能更齐全的布尔标志。尽管它也无法拷贝构造或拷贝赋值,但还是能依据非原子布尔量创建其对象,也能接受非原子布尔量的赋值:
std::atomic<bool> b(true);
b = false;
store()是存储操作,可以向原子对象写入值。load()是载入操作,可以读取原子对象的值。exchange()是“读-改-写”操作,它获取原有的值,然后用自行选定的新值作为替换。
std::atomic<bool> b;
bool x = b.load();
b.store(true);
x = b.exchange(false);
compare_exchange_weak()与compare_exchange_strong()被称为“比较-交换”操作,它们的作用是:使用者给定一个期望值,原子变量将它和自身的值进行比较,如果相等,就存入另一既定的值;否则,更新期望值所属的变量,向它赋予原子变量的值。“比较-交换”操作返回布尔类型,如果完成了保存动作(前提是两值相等),则返回true,否则返回false。对于compare_exchange_weak(),即使原子变量的值等于期望值,保存动作还是有可能失败,在这种情形下,原子变量维持原值不变,函数返回false。原子化的“比较-交换”必须由一条指令单独完成,而某些处理器没有这种指令,无从保证该操作按原子化方式完成。要实现“比较-交换”,负责的线程则须改为连续运行一系列指令,但在这些计算机上,只要出现线程数量多于处理器数量的情形,线程就有可能执行到中途因系统调度而切出,导致操作失败。这种败因不是变量值本身存在问题,而是函数执行时机不对,所以compare_exchange_weak()往往必须配合循环使用。
bool expected = false;
extern atomic<bool> b;
while(!b.compare_exchange_weak(expected,true) && !expected);
2.3、操作std::atomic<T*>
除了std::atomic<bool>所支持的操作外,std::atomic<T*>还支持算术形式的指针运算。fetch_add()和fetch_sub()分别就对象中存储的地址进行原子化加减,然后返回原来的地址。另外,该原子类型还具有包装成重载运算符的+=和-=,以及++和--的前后缀版本,这些运算符作用在原子类型之上,效果与作用在内建类型上一样。
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]);
2.4、操作标准整数原子类型
在std::atomic<int>这样的整数原子类型上,除了std::atomic<T*>所支持的操作外,还支持fetch_and()、fetch_or()、fetch_xor()操作,也支持对应的&=、|=、^=复合赋值形式。
2.5、泛化的std::atomic<>类模板
除了前文的标准原子类型,使用者还能利用泛化模板,依据自定义类型创建其它原子类型。然而,对于某个自定义的类型UDT,必须要满足一定条件才能具现化出std::atomic<UDT>:
- 必须具有平实拷贝赋值运算符(平直、简单的原始内存赋值及其等效操作)。若自定义类型具有基类或非静态数据成员,则它们同样必须具备平实拷贝赋值运算符。
- 不得含有虚函数,也不可以从虚基类派生得出。
- 必须由编译器代其隐式生成拷贝赋值运算符。
由于以上限制,赋值操作不涉及任何用户编写的代码,因此编译器可以借用memcpy()或采取与之等效的行为完成它。另外值得注意的是,“比较-交换”操作采取的是逐位比较运算,效果等同于直接使用memcmp()函数。
3、内存顺序
编译器优化代码时可能会进行指令重排,而且CPU执行指令时也可能会乱序执行,所以代码的执行顺序不一定和书写顺序一致。例如下面的代码可能会按照如表所示的顺序执行,从而引发断言错误。可以看出,指令重排在单线程环境下不会造成逻辑错误,但在多线程环境下可能会造成逻辑错误。
int a = 0;
bool flag = false;
void func1()
{
a = 1;
flag = true;
}
void func2()
{
if (flag)
{
assert(a == 1);
}
}
std::thread t1(func1);
std::thread t2(func2);
t1.join();
t2.join();
| step | 线程t1 | 线程t2 |
|---|---|---|
| 1 | flag = true |
|
| 2 | if (flag) |
|
| 3 | assert(a == 1) |
|
| 4 | a = 1 |
内存顺序的作用,本质上是要限制单个线程中的指令顺序,从而解决多线程环境下可能出现的问题。原子类型上的操作服从6种内存顺序,在不同的CPU架构上,这几种内存模型也许会有不同的运行开销。
enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
};
memory_order_seq_cst
这是所有原子操作的内存顺序参数的默认值,语义上要求底层提供顺序一致性模型,不存在任何重排,可以解决一切问题,但是效率最低。
memory_order_release / memory_order_acquire / memory_order_consume
release操作可以阻止这个调用之前的读写操作被重排到后面去;acquire操作则可以保证这个调用之后的读写操作不会重排到前面去;consume操作比acquire操作宽松一些,它只保证这个调用之后的对原子变量有依赖的操作不会被重排到前面去。release与acquire/consume操作需要在同一个原子对象上配对使用,例如:
std::atomic<int> a;
std::atomic<bool> flag;
void func1()
{
a = 1;
flag.store(true, memory_order_release);
}
void func2()
{
if (flag.load(memory_order_acquire))
{
assert(a == 1);
}
}
memory_order_acq_rel
兼具acquire和release的特性。
memory_order_relaxed
只保证原子类型的成员函数操作本身是不可分割的,但是对于顺序性不做任何保证。
三类操作支持的内存顺序如下表所示:
| 存储(store)操作 | 载入(load)操作 | “读-改-写”(read-modify-write)操作 | |
|---|---|---|---|
| memory_order_seq_cst | Y | Y | Y |
| memory_order_release | Y | Y | |
| memory_order_acquire | Y | Y | |
| memory_order_consume | Y | Y | |
| memory_order_acq_rel | Y | ||
| memory_order_relaxed | Y | Y | Y |
《C++并发编程实战》读书笔记(4):原子变量的更多相关文章
- Java并发编程实战 读书笔记(一)
最近在看多线程经典书籍Java并发变成实战,很多概念有疑惑,虽然工作中很少用到多线程,但觉得还是自己太弱了.加油.记一些随笔.下面简单介绍一下线程. 一 线程与进程 进程与线程的解释 个人觉 ...
- Java并发编程实战 读书笔记(二)
关于发布和逸出 并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了.这是危及到线程安全的,因为其他线程有可能通过这个 ...
- Java并发编程实战 第15章 原子变量和非阻塞同步机制
非阻塞的同步机制 简单的说,那就是又要实现同步,又不使用锁. 与基于锁的方案相比,非阻塞算法的实现要麻烦的多,但是它的可伸缩性和活跃性上拥有巨大的优势. 实现非阻塞算法的常见方法就是使用volatil ...
- 《java并发编程实战》笔记
<java并发编程实战>这本书配合并发编程网中的并发系列文章一起看,效果会好很多. 并发系列的文章链接为: Java并发性和多线程介绍目录 建议: <java并发编程实战>第 ...
- 《Java并发编程实战》笔记-锁与原子变量性能比较
如果线程本地的计算量较少,那么在锁和原子变量上的竞争将非常激烈.如果线程本地的计算量较多,那么在锁和原子变量上的竞争会降低,因为在线程中访问锁和原子变量的频率将降低. 在高度竞争的情况下,锁的性能将超 ...
- Java多线程编程实战读书笔记(一)
多线程的基础概念本人在学习多线程的时候发现一本书——java多线程编程实战指南.整理了一下书中的概念制作成了思维导图的形式.按照书中的章节整理,并添加一些个人的理解.
- 《Java并发编程实战》笔记-OneValueCache与原子引用技术
/** * NumberRange * <p/> * Number range class that does not sufficiently protect its invariant ...
- Java并发编程实践读书笔记(5) 线程池的使用
Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到 ...
- Java并发编程实践(读书笔记) 任务执行(未完)
任务的定义 大多数并发程序都是围绕任务进行管理的.任务就是抽象和离散的工作单元. 任务的执行策略 1.顺序的执行任务 这种策略的特点是一般只有按顺序处理到来的任务.一次只能处理一个任务,后来其它任 ...
- Java并发编程实践读书笔记(2)多线程基础组件
同步容器 同步容器是指那些对所有的操作都进行加锁(synchronize)的容器.比如Vector.HashTable和Collections.synchronizedXXX返回系列对象: 可以看到, ...
随机推荐
- Bootstrap 栅栏布局中 col-xs-*、col-sm-*、col-md-*、col-lg-* 区别及使用方法 _2021-11-10
Bootstrap 栅栏布局中 col-xs-.col-sm-.col-md-.col-lg- 区别及使用方法 全文转自:https://www.cnblogs.com/tangbohu2008/p/ ...
- Python 中的抽象类和接口类
[抽象类] 抽象类是一个特殊的类,只能被继承,不能被实例化.它主要用于作为其他类的基类或模板. 抽象类可以包含抽象方法和具体方法.在抽象类中定义的抽象方法必须在子类中进行实现. from abc im ...
- DataGrip 2024.2.2 最新安装教程(附激活-2099年~)
下载 DataGrip 2024.2.2 版本的安装包. 下载补丁https://pan.quark.cn/s/fcc23ab8cadf 检查 免责声明:本文中的资源均来自互联网,仅供个人学习和交流使 ...
- [离线计算-Spark|Hive] HDFS小文件处理
背景 HDFS 小文件过多会对hadoop 扩展性以及稳定性造成影响, 因为要在namenode 上存储维护大量元信息. 大量的小文件也会导致很差的查询分析性能,因为查询引擎执行查询时需要进行太多次文 ...
- 2-3 C++复合类型
目录 2.3.1 引用(References) 2.3.2 指针(Pointers) 关于指针 指针操作 其它事项 空指针的三种表示 void* 指针 易混淆的符号 指针的值(地址)的四种状态 对比与 ...
- salesforce零基础学习(一百四十一)刷新dev sandbox需要强制group
本篇参考:https://help.salesforce.com/s/articleView?id=sf.data_sandbox_selective_access.htm&type=5 背景 ...
- jenkins集成cucumber-resport报告
需要安装的jenkins插件-Cucumber Reports jenkins版本:2.273 jenkins插件下载地址:点击下载 下载插件后通过jenkins插件管理上传已下载好的插件即可 等待j ...
- 设计模式(17)-Chain of Responsibility Pattern
行为模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化.行为模式不仅仅是关于类和对象的,而且是关于它们之间的相互作用的. 行为模式分为类的行为模式和对象的行为模式两 ...
- SharpZipLib解压.tar.gz
上一篇文章我们说了如何用SharpZipLib来压缩.tar.gz,这篇文章我们来看看如何解压. 直接上代码: using ICSharpCode.SharpZipLib.GZip; using IC ...
- Educational Codeforces Round 65 (Rated for Div
D - Bicolored RBS 给定一个括号序列,现在你必须对每一个括号涂成蓝色或红色,要求使得涂完后的红色括号和蓝色括号序列都必须是合法的括号序列,设红色括号形成的的括号序列的深度为\(dep_ ...