《C++并发编程实战》第5章 C++内存模型和原子操作

本书的章节安排

在这里插入图片描述

C++中的位域

在这里插入图片描述

在这里插入图片描述

5.1 C++中的原子操作及其类型

5.2.1 标准原子类型

is_lock_free() ,准许使用者判定某一给定类型上的操作是能由原子指令(atomic instruction)直接实现(x.is_lock_free()返回true)​,还是要借助编译器和程序库的内部锁来实现(x.is_lock_free()返回false)​。这一功能可在许多情形中派上大用场,原子操作的关键用途是取代需要互斥的同步方式。但是,假如原子操作本身也在内部使用了互斥,就很可能无法达到所期望的性能提升,而更好的做法是采用基于互斥的方式,该方式更加直观且不易出错。无锁数据结构正属于这种情况,我们将在第7章讨论。

在这里插入图片描述
在这里插入图片描述

  • 原子类型的新旧别名不要混用
    在这里插入图片描述

原子类型的第二个参数取值:

存储(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。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.2.2 操作std::atomic_flag

std::atomic_flag类型的对象必须由宏ATOMIC_FLAG_INIT初始化。也是唯一保证无锁的原子类型

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

  • 使用std::atomic_flag()实现无锁

#include <iostream>
#include <list>
#include <algorithm>
#include <thread>
#include <future>

class SpinLockMutex
{
    std::atomic_flag flag = ATOMIC_FLAG_INIT;

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

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

    void unlock()
    {
        flag.clear(std::memory_order_release);
    }
};

5.2.3 操作std::atomic

奇怪的赋值操作

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
atomic 的赋值运算符不返回引用,是为了防止你“刚赋完值,却被别人抢先改了,然后你又把改后的值当结果用”

“比较-交换”​(compare-exchange)​

在这里插入图片描述
在这里插入图片描述
compare_exchange_weak(),返回false有有两种情况,一种是正常现象即和预期值不相等,第二种是原子化的比较-交换必须由一条指令单独完成,而某些处理器没有这种指令,无从保证该操作按原子化方式完成。要实现比较-交换,负责的线程则须改为连续运行一系列指令,但在这些计算机上,只要出现线程数量多于处理器数量的情形,线程就有可能执行到中途因系统调度而切出,导致操作失败,从而返回false,所以为了避免第二🀄情况的出现,我们一般使用对于compare_exchange_weak()使用while

在这里插入图片描述



// 假设 atomic_val 的初始值是 1
int expected = 2;
int desired = 3;

// 循环直到成功
while (!atomic_val.compare_exchange_weak(expected, desired))
{
    // 第一次循环:atomic_val=1, expected=2 -> 失败,expected被更新为1,此时expected=1,返回false
    // 第二次循环:atomic_val=1, expected=1 -> 成功,atomic_val被更新为3,返回true
}

在这里插入图片描述

成功或失败

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

fetch_add()和fetch_sub()都是“读-改-写”操作

在这里插入图片描述
在这里插入图片描述
整数原子类型上的相关操作尚不算全面,但已经近乎完整,所缺少的重载运算符仅仅是乘除与位移

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

std::atomic 只适用于“把对象当作一整块不可分割内存来比较和交换,而完全不关心其业务语义”的平凡数据类型。
在这里插入图片描述
在这里插入图片描述
std::atomic 允许你对自定义类型做原子操作,但仅限于那些结构简单、能被 memcpy 安全复制的类型。它的比较是位级别的,不受你写的 == 影响,也不能用于复杂对象。如果不满足条件,就别硬用,改用 std::mutex 更安全。

5.3 同步操作和强制次序

同步关系只存在于原子类型的操作之间。

先后一致次序(memory_order_seq_cst)​、获取-释放次序(memory_order_consume、memory_order_acquire、memory_order_release和memory_order_acq_rel)​、宽松次序(memory_order_relaxed)​。

在这里插入图片描述

对于memory_order_acquire和memory_order_release的重要纠偏

在这里插入图片描述
read_x_then_y执行到if时,一定保证x为true。read_y_then_x执行到if时,一定保证y为true。所以最终的x和y都会为true,注意这是最终,也就是aeesrt时,但是!!!!在read_x_then_y和read_y_then_x两个线程并不能保证执行到if时会使if为真。write_x()和write_y()并没有一个前后关系。如果两个有前后关系的话比如写x在前,那么执行到read_y_then_x线程一定可以使z++

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述

注意relaxed和released配合可以实现同步

上面的两个同步变量sync1和sync2变为1个:

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

std::atomic<int> data[5];
std::atomic<int> sync(0); // 0 → 1 → 2

void thread_1()
{
    data[0].store(42,  std::memory_order_relaxed);
    data[1].store(97,  std::memory_order_relaxed);
    data[2].store(17,  std::memory_order_relaxed);
    data[3].store(-141,std::memory_order_relaxed);
    data[4].store(2003,std::memory_order_relaxed);

    sync.store(1, std::memory_order_release);
}

void thread_2()
{
    int expected = 1;

    // 等待 sync == 1
    while (!sync.compare_exchange_strong(
        expected,
        2,
        std::memory_order_acq_rel,
        std::memory_order_relaxed))
    {
        expected = 1; // CAS失败需恢复 expected
        // 自旋直到成功把 1 改成 2
    }
}

void thread_3()
{
    // 等待 sync == 2
    while(sync.load(std::memory_order_acquire) != 2);

    assert(data[0].load(std::memory_order_relaxed)==42);
    assert(data[1].load(std::memory_order_relaxed)==97);
    assert(data[2].load(std::memory_order_relaxed)==17);
    assert(data[3].load(std::memory_order_relaxed)==-141);
    assert(data[4].load(std::memory_order_relaxed)==2003);
}

int main()
{
    std::thread t1(thread_1);
    std::thread t2(thread_2);
    std::thread t3(thread_3);

    t1.join();
    t2.join();
    t3.join();
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
“保序语义”(Sequential Consistency,即 memory_order_seq_cst)

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.3.5 栅栏

栅栏具备多种操作,用途是强制施加内存次序,却无须改动任何数据。通常,它们与服从memory_order_relaxed次序的原子操作组合使用。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

普通变量本身不具备内存顺序语义
但只要它们“夹在原子操作中间”,
就会被“强制拖进”内存模型的秩序里

什么是“先行关系(happens-before)”?
不是“时间先后”,而是:
A happens-before B ⇒ A 的结果对 B 必须可见

在这里插入图片描述

前五章的总结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值