部分文章内容为公开资料查询整理,原文出处可能未标注,如有侵权,请联系我,谢谢。邮箱地址:gnivor@163.com ►►►需要气球么?请点击我吧!

C++笔记-Atomic原子操作/CAS(Compare and Swap)

0. 参考资料

  1. C++ atomic http://www.cplusplus.com/reference/atomic/atomic/
  2. C++ 原子操作库 std::atomic https://www.apiref.com/cpp-zh/cpp/atomic/atomic/compare_exchange.html
  3. CAS https://www.cnblogs.com/muhe221/articles/5089918.html
  4. c++并发编程3. CAS原语 https://zhuanlan.zhihu.com/p/56055215
  5. C++11:原子交换函数compare_exchange_weak和compare_exchange_strong https://blog.csdn.net/feikudai8460/article/details/107035480/
  6. 理解memory order https://blog.csdn.net/jiang4357291/article/details/110753759
  7. C++11中的内存模型下篇 - C++11支持的几种内存模型 https://www.codedump.info/post/20191214-cxx11-memory-model-2/
  8. 调试经验 | C++ memory order和一个相关的稳定性问题 https://juejin.cn/post/6844904096671989773
  9. std::memory_order https://en.cppreference.com/w/cpp/atomic/memory_order
  10. std::memory_order https://zh.cppreference.com/w/cpp/atomic/memory_order

1. 背景

多线程读写非线程安全的数据结构时,为了保证结果正确性,一种方式是对数据结构加锁后进行读写。为了解决加锁带来的性能损耗问题,可使用CAS。

2. CAS

Compare-and-Swap (CAS)是用于多线程以实现同步的原子指令。它将存储位置的内容与给定值进行比较,当它们逐位相等,才将该存储位置的内容修改为新的给定值。整个流程为一个原子操作。

2.1 C++的CAS方法

compare_exchange_weak
compare_exchange_strong
其位于atomic库中 http://www.cplusplus.com/reference/atomic/atomic/

2.2 std::atomic的使用

定义一个原子对象,以链表Node为例:

struct Node {
  int value;
  Node *next;
};
std::atomic<Node *> list_head;

atomic中的主要方法有:

方法名 功能 备注
is_lock_free Is lock-free
store Modify contained value
load Read contained value
operator T Access contained value
exchange Access and modify contained value
compare_exchange_weak Compare and exchange contained value (weak)
compare_exchange_strong Compare and exchange contained value (strong)

2.3 CAS函数说明

2.3.1compare_exchange_weak

bool compare_exchange_weak (T& expected, T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_weak (T& expected, T val, memory_order sync = memory_order_seq_cst) noexcept;
bool compare_exchange_weak (T& expected, T val, memory_order success, memory_order failure) volatile noexcept;
bool compare_exchange_weak (T& expected, T val, memory_order success, memory_order failure) noexcept;

2.3.2compare_exchange_strong

bool compare_exchange_strong (T& expected, T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_strong (T& expected, T val, memory_order sync = memory_order_seq_cst) noexcept;
bool compare_exchange_strong (T& expected, T val, memory_order success, memory_order failure) volatile noexcept;
bool compare_exchange_strong (T& expected, T val, memory_order success, memory_order failure) noexcept;

函数释义:
将原子对象包含的值的内容与expected值进行比较:
-如果为true,则用val替换原子对象包含的值(类似于store)。
-如果为false,则用原子对象包含的值替换expected

此函数直接将包含值的物理内容与预期值的内容进行比较
compare_exchange_strong不同,compare_exchange_weak返回false时,仍然可能是expected值原子对象包含的对象相等的情况。对于某些循环算法来说,这可能是可以接受,并且在某些平台上可能会有明显更好的性能。对于这种情况,函数返回false,但没有修改expected

对于非循环算法,通常首选compare_exchange_strong

方法参数释义
expected: 对一个对象的引用,该对象的值与原子对象包含的值进行比较,如果不匹配,该对象可能会被包含的值覆盖。
val: 当expected原子对象包含的值匹配时,copy val到原子对象内包含的值
sync: CAS操作的同步模式。参数类型为memory_order,为一组枚举值,其具体功能后文介绍

2.3.3例程

Demo: 线程安全的无锁链表

// a simple global linked list:
struct Node {
  int value;
  Node *next;
};

class ConcurrentLinkList {
public:
  ConcurrentLinkList() { list_head = nullptr; }

  void append(int val) { // append an element to the list
    Node *old_head = list_head.load();
    Node *new_node = new Node{val, old_head};

    // equivalent to: list_head = new_node, but in a thread-safe way:
    while (!list_head.compare_exchange_weak(old_head, new_node)) {
      new_node->next = old_head;
    }
  }

  void clean() {
    Node *it;
    while (it = list_head) {
      list_head = it->next;
      delete it;
    }
  }

public:
  std::atomic<Node *> list_head;
};
点击打开完整示例
#include <atomic>         // std::atomic
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <vector>         // std::vector

// a simple global linked list:
struct Node {
  int value;
  Node *next;
};

class ConcurrentLinkList {
public:
  ConcurrentLinkList() { list_head = nullptr; }

  void append(int val) { // append an element to the list
    Node *old_head = list_head.load();
    Node *new_node = new Node{val, old_head};
    // equivalent to: list_head = new_node, but in a thread-safe way:
    while (!list_head.compare_exchange_weak(old_head, new_node)) {
      new_node->next = old_head;
    }
  }

  void clean() {
    Node *it;
    while (it = list_head) {
      list_head = it->next;
      delete it;
    }
  }

public:
  std::atomic<Node *> list_head;
};

int main() {
  // spawn 10 threads to fill the linked list:
  ConcurrentLinkList list;
  std::vector<std::thread> threads;
  for (int i = 0; i < 10; ++i) {
    threads.push_back(std::thread(&ConcurrentLinkList::append, &list, i));
  }
  for (auto &th : threads) {
    th.join();
  }

  // print contents:
  for (Node *it = list.list_head; it != nullptr; it = it->next) {
    std::cout << it->value << ' ';
  }
  std::cout << '\n';

  // cleanup:
  list.clean();
  return 0;
}

compare_exchange_strong使用的结果

点击打开完整示例
#include <atomic>
#include <iostream>
 
std::atomic<int>  ai;
 
int  tst_val= 4;
int  new_val= 5;
bool exchanged= false;
 
void valsout()
{
    std::cout << "ai= " << ai
	      << "  tst_val= " << tst_val
	      << "  new_val= " << new_val
	      << "  exchanged= " << std::boolalpha << exchanged
	      << "\n";
}
 
int main()
{
    ai= 3;
    valsout();
 
    // tst_val != ai   ==>  tst_val 被修改
    exchanged= ai.compare_exchange_strong( tst_val, new_val );
    valsout();
 
    // tst_val == ai   ==>  ai 被修改
    exchanged= ai.compare_exchange_strong( tst_val, new_val );
    valsout();
}

结果:

ai= 3  tst_val= 4  new_val= 5  exchanged= false
ai= 3  tst_val= 3  new_val= 5  exchanged= false
ai= 5  tst_val= 3  new_val= 5  exchanged= true

3. memory order

compare_exchange_weakcompare_exchange_strong方法的参数 sync 表示CAS操作的同步模式,参数类型为memory_order,为一组枚举值,以下是枚举类型memory_order的所有可能值:

value memory order description
memory_order_relaxed Relaxed 没有同步或顺序制约,仅对此操作要求原子性
memory_order_consume Consume 1. 对当前要读取的内存施加 release 语义(store),在代码中这条语句后面所有与这块内存有关的读写操作都无法被重排到这个操作之前
2. 在这个原子变量上施加 release 语义的操作发生之后,consume 可以保证读到所有在 release 前发生的并且与这块内存有关的写入
memory_order_acquire Acquire 1. 向前保证,本线程中所有读写操作都不能重排到memory_order_acquire的load之前
2. 其他线程中所有memory_order_release的写操作都对当前线程可见
memory_order_release Release 1. 向后保证,本线程中所有读写操作都不能重排到memory_order_acquire的store之后
2. 本线程中的所有写都对其他对同一atomic变量带有 memory_order_acquire的线程可见
3. 本线程中的所有写都对其他所有有依赖且consume该变量的线程可见
memory_order_acq_rel Acquire/Release 1. 是release+acquire的结合,前后都保证,本线程中所有读写操作既不能重排到memory_order_acquire的load之前也不能到之后
2. 其他线程的memory_order_release写在本线程写之前都是可见的
3. 本线程的的写对其他线程的memory_order_acquire读都是可见的
memory_order_seq_cst Sequentially consistent 1. 顺序一致性
2. 如果是读取就是 acquire 语义,如果是写入就是 release 语义,如果是读取+写入就是 acquire-release 语义
3. 同时会对所有使用此 memory order 的原子操作进行同步,所有线程看到的内存操作的顺序都是一样的,就像单个线程在执行所有线程的指令一样

可参考这篇文章的图: https://blog.csdn.net/jiang4357291/article/details/110753759

当多个线程访问一个原子对象时,所有原子操作的行为特性:在任何其他原子操作可以访问该对象之前,每个原子操作都是在该对象上完全执行的。这保证了这些对象上没有数据竞争。
但是,每个线程可能会对原子对象本身以外的内存位置执行操作:这些其他操作可能会对其他线程产生明显的副作用。memory_order这种类型的参数允许为操作指定内存顺序(memory order),该操作确定如何在线程之间同步这些(可能是非原子的)可见副作用。
在 C11/C++11 中,引入了六种不同的 memory order,可以让程序员在并发编程中根据自己需求尽可能降低同步的粒度,以获得更好的程序性能。这六种 order 分别是:
relaxed, acquire, release, consume, acq_rel, seq_cst

Relaxed ordering 示例

点击打开完整示例
#include <atomic>
#include <thread>
#include <assert.h>

std::atomic<bool> x,y;

void thread1_fun() {
    x.store(true, memory_order_relaxed);
    y.store(true, memory_order_relaxed);
}

void thread2_fun() {
    while(!y.load(memory_order_relaxed));
        assert(x.load(memory_order_relaxed));
}

int main() {
    x=false;
    y=false;
    std::thread t1(thread1_fun);
    std::thread t2(thread2_fun);
    t1.join();
    t2.join();
    return 0;
}

结果
thread2_fun 内assert可能失败。 因为thread1中的x和y的store可能会重排(因为这个重排并不影响在thread1内的执行的结果)

Release-consume ordering 示例

点击打开完整示例
#include <atomic>
#include <thread>
#include <assert.h>

atomic<int> a;
atomic<int> b;

void fun1() {
  a.store(1, memory_order_relaxed);
  b.store(2, memory_order_release);
}

void fun2() {
    while (b.load(memory_order_consume) != 2) {
        // do nothing
    }
   assert(a == 1);
}

int main() {
    a=0;
    b=0;
    std::thread t1(fun1);
    std::thread t2(fun2);
    t1.join();
    t2.join();
    return 0;
}

结果:fun2() 内assert可能失败。 线程1中a和b并不相关,a的写入就可能被重排到b之后,这样在b线程load时就有可能a还未store,此时a=0,断言失败。(据[8]说memory_order_consume的设计有缺陷,建议大家不要使用)

Release-acquire ordering示例

点击打开完整示例

memory_order_acquire禁止了该load操作之后的所有读写操作(不管是原子对象还是非原子对象)被重排到它之前去运行。[8]
memory_order_acquire

memory_order_release禁止了该store操作之前的所有读写操作(不管是原子对象还是非原子对象)被重排到它之后去运行。[8]
memory_order_release

当flag.load在时间上晚发生于flag.store时,Thread 1上flag.store之前的所有操作对于Thread 2上flag.load之后的所有操作都是可见的。如果flag.load发生的时间早于flag.store,那么两个线程间则不拥有任何数据可见性。[8]
Release-acquire

#include <thread>
#include <atomic>
#include <cassert>
 
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
 
void write_x()
{
    x.store(true, std::memory_order_release);
}
 
void write_y()
{
    y.store(true, std::memory_order_release);
}
 
void read_x_then_y()
{
    while (!x.load(std::memory_order_acquire))
        ;
    if (y.load(std::memory_order_acquire)) {
        ++z;
    }
}
 
void read_y_then_x()
{
    while (!y.load(std::memory_order_acquire))
        ;
    if (x.load(std::memory_order_acquire)) {
        ++z;
    }
}
 
int main()
{
    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); 
}

结果:不少博客内认为assert可能失败,解释:
write_x线程中x的store使用了memory_order_release,和read_x_then_y中的load配对,保证了read_x_then_y线程中load x时能看到其他线程对x的所有release,所以load y之前x一定为true退出了循环
同理,read_y_then_x线程中,load x之前y一定为true
但是,read_x_then_y中y load之前y为0和1确都是有可能的,因为y.load(std::memory_order_acquire)只是保证了load y之前其他线程所有对y的release对当前都是可见的,但是完全有可能write_y线程还没调度到,y就是0
同理,read_y_then_x中load x也无法保证x必定不为0,所以z最终就可能为0,断言失败
文章[7] https://www.codedump.info/post/20191214-cxx11-memory-model-2/ 内也有相同解释
这里想实际有点存疑:

read_x_then_y 和 read_y_then_x
执行到if判断时,x/y至少有1个已经store完成了并且对所有acquire线程可见了。所以if判断至少有1个会执行到。 assert 必定成功。
解释一下,只可能存在两种情况:
1:read_x_then_y() 内的 if (y.load(std::memory_order_acquire)) 先执行
2:read_y_then_x() 内的 if (x.load(std::memory_order_acquire)) 先执行
如果是情况1,则x必定已经load完了,所以2内的if判断成功
如果是情况2,则y必定已经load完了,所以1内的if判断成功

文章[7]的评论内也有人有相同疑问,不过也有评论说明:
zh.cppreference.com/w/cpp/atomic/memory_order 的“释放获得顺序”那一节里面说道到“同步仅建立在释放和获得同一原子对象的线程之间。其他线程可能看到与被同步线程的一者或两者相异的内存访问顺序”,也就是说“在 thread c 可能看到是先写 x 后写 y,但在 thread d 可能是先写 y 后写 x”
这部分待证实

Sequentially-consistent ordering示例

点击打开完整示例
#include <thread>
#include <atomic>
#include <cassert>
 
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
 
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()
{
    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);  // 决不发生
}
posted @ 2022-03-27 21:57  流了个火  阅读(7019)  评论(0编辑  收藏  举报
►►►需要气球么?请点击我吧!►►►
View My Stats