《C++并发编程实战》读书笔记(4):原子变量

这篇具有很好参考价值的文章主要介绍了《C++并发编程实战》读书笔记(4):原子变量。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1、标准原子类型

标准原子类型的定义位于头文件<atomic>内。原子操作的关键用途是取代需要互斥的同步方式,但假设原子操作本身也在内部使用了互斥,就很可能无法达到期望的性能提升。有三种方法来判断一个原子类型是否属于无锁数据结构:

  • 所有标准原子类型(std::atomic_flag除外,因为它必须采取无锁操作)都具有成员函数is_lock_free(),若它返回true则表示给定类型上的操作是能由原子指令直接实现的,若返回false则表示需要借助编译器和程序库的内部锁来实现。
  • C++程序库提供了一组宏:ATOMIC_BOOL_LOCK_FREEATOMIC_CHAR_LOCK_FREEATOMIC_CHAR16_T_LOCK_FREEATOMIC_CHAR32_T_LOCK_FREEATOMIC_WCHAR_T_LOCK_FREEATOMIC_SHORT_LOCK_FREEATOMIC_INT_LOCK_FREEATOMIC_LONG_LOCK_FREEATOMIC_LLONG_LOCK_FREEATOMIC_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_flagstd::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

    只保证原子类型的成员函数操作本身是不可分割的,但是对于顺序性不做任何保证。

三类操作支持的内存顺序如下表所示:文章来源地址https://www.toymoban.com/news/detail-695019.html

存储(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):原子变量的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • JUC并发编程学习笔记(十九)原子引用

    带版本号的原子操作! 解决ABA问题,引入原子引用(乐观锁思想) AtomicStampedReference类解决ABA问题 所有相同类型的包装类对象之间值的比较全部使用equals方法比较 Integer使用了对象缓存机制,默认范围是-128至127,推荐使用静态工厂方法valueOf获取对象实例,而不是new,因为v

    2024年02月05日
    浏览(54)
  • java并发编程之美第五章读书笔记

    CopyOnWriteArrayList 线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)进行的,也就是写时复制策略 类图 每一个对象里面有一个array数组进行存放具体的元素,ReentrantLock独占锁对象用来保证同时只有一个线程对array进行修改,这里只要记得ReentrantLock是独占锁

    2024年02月03日
    浏览(43)
  • 《C++高级编程》读书笔记(七:内存管理)

    1、参考引用 C++高级编程(第4版,C++17标准)马克·葛瑞格尔 2、建议先看《21天学通C++》 这本书入门,笔记链接如下 21天学通C++读书笔记(文章链接汇总) 1. 使用动态内存 1.1 如何描绘内存 在本书中,内存单元表示为一个带有标签的框,该标签表示这个内存对应的变量名,方

    2024年02月08日
    浏览(86)
  • c++并发编程实战-第4章 并发操作的同步

    想象一种情况:假设晚上坐车外出,如何才能确保不坐过站又能使自己最轻松? 这种方式存在双重浪费: 线程 th1(wait_for_flag)须不断查验标志,浪费原本有用的处理时间,这部分计算资源原本可以留给其他线程使用。 线程 th1(wait_for_flag)每次循环都需要给互斥上锁,导致

    2024年02月08日
    浏览(39)
  • 《Java并发编程实战》课程笔记(十二)

    对账系统的业务简化后: 首先用户通过在线商城下单,会生成电子订单,保存在订单库; 之后物流会生成派送单给用户发货,派送单保存在派送单库。 为了防止漏派送或者重复派送,对账系统每天还会校验是否存在异常订单。 目前对账系统的处理逻辑是首先查询订单,然后

    2024年02月08日
    浏览(42)
  • 《Java并发编程实战》课程笔记(二)

    在单核时代,所有的线程都是在一颗 CPU 上执行,CPU 缓存与内存的数据一致性容易解决。 因为所有线程都是操作同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的。 一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。 多核

    2024年02月06日
    浏览(36)
  • 《Java并发编程实战》课程笔记(九)

    信号量模型还是很简单的,可以简单概括为:一个计数器,一个等待队列,三个方法。 在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down() 和 up()。 init():设置计数器的初始值。 down():计

    2024年02月07日
    浏览(35)
  • C++ 并发编程实战 第二章 线程管控

    线程通过构建 std::thread 对象而自动启动 ,该对象指明线程要运行的任务。 对应复杂的任务,可以使用函数对象。 一旦启动了线程,我们就需明确是要等待它结束(与之汇合 join() ),还是任由它独自运行(与之分离 detach() ) ❗❗❗ 同一个线程的 .join() 方法不能被重复调用

    2023年04月08日
    浏览(41)
  • 并发编程08:原子操作类

    Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 AtomicInteger :整型原子类 At

    2024年02月04日
    浏览(35)
  • 并发编程-JUC-原子类

    JUC 整体概览 原子类 基本类型-使用原子的方式更新基本类型 AtomicInteger:整形原子类 AtomicLong:长整型原子类 AtomicBoolean :布尔型原子类 引用类型 AtomicReference:引用类型原子类 AtomicStampedReference:原子更新引用类型里的字段原子类 AtomicMarkableReference :原子更新带有标记位的引

    2024年02月21日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包