C++多线程编程之原子变量/操作,内存序

一. 背景

在多线程编程中锁,条件变量都是多线程编程里面的内容,原子变量也是里面重要的补充。我们程序运行时会乱序执行,并不是按照我们程序书写的代码顺序那样去执行。为什么会出现这样的情况呢,主要是因为编译器优化,编译器为了执行效率的提高,会将代码执行顺序重新排列

这也就给我们多线程编程带来了挑战,特别是现在是CPU多核的时代,当多个线程并行执行的时候,如果没有恰当的同步机制,不同CPU核看到的线程间共享数据是有差别的。在单核情况下,多线程也会遇到此问题。在单核一个线程下,程序执行语句也会重新排序,只是保证最后执行的结果不会改变。

同时CPU核心基本上都有缓存加速机制,这样更增加了多线程编程的难度。

传统上的多线程解决方法是加锁,如mutex等,但是存在下面弊端:

1. 性能损失,加锁/解锁操作对于一些简单语句的保护是比较耗性能的。

2. 死锁风险,编程中,如果锁中再套相同的锁,会出现死锁,代码复杂情况下,不好维护。

如果上面两种情况,大家觉得能够承担,就尽量用锁的方法多线程编程解决共享数据不一致问题。

而原子变量/操作的方法是在上述情况不能承担的情况下考虑使用的方法,这个方法编程会比较复杂。

二. 原子变量/操作的定义

原子变量/操作,顾名思义就是不可再分的变量/操作,即引入一些变量或者函数,保证这些变量或者函数所对应的操作语句是不可再分的,在多个线程里面,这些变量是都可见的,没有缓存机制影响,并且代码在内存中执行顺序是得到保证的。

其表达形式如下:

#include <atomic>

template <typename T>

std::atomic<T> myVar();

2.1 原子变量的操作

  • load() : 从原子变量里面读出原子变量的值
  • store() : 将值写入到原子变量里面
  • fetch_add() 和 fetch_sub : 对原子变量读出来后并且做修改操作,返回修改之前的值
  • compare_exchange_weak(expected&, val, memory_order_for_success, memory_order_for_failure)   :   比较原子变量与expected的值,如果他们是相同的,则用val的值替换原子变量的值,更新成功了就返回true, 更新失败就返回false.;否则如果和expected的值不相同,则用当前的原子变量的值赋值给expected变量。
  • compare_exchange_strong(expected&, val, memory_order_for_sucess, memory_order_for_failure)   :   比较原子变量与expected的值,如果他们是相同的,则用val的值替换原子变量的值,更新成功了就返回true, 更新失败就返回false.;否则如果和expected的值不相同,则用当前的原子变量的值赋值给expected变量。

大家这里估计会问,compare_exchange_weak和compare_exchange_strong的区别:

1. compare_exchange_weak会比compare_exchange_strong的性能好。

2. compare_exchange_weak的返回值不是每次都可靠准确的,有可能里面expected与原子变量相同,并且去做替换操作了,这个函数还是会返回一个false,这个可以理解为此原子操作还没有执行完成,为了不阻塞执行先返回一个false;所以这个操作一般会在一个不断循环的逻辑里执行,直到返回为true,才代表这个语句执行完成了。当然如果循环里面一直返回是false, 原子操作里面存在条件不满足确实应该返回false.

3. compare_exchange_strong会比comare_exchange_weak性能差,但是保证一次性操作是准确和可靠的。

4. compare_exchange_weak 设计这样子方便在一个线程里面循环去观察某个共享变量值,这样在一个线程里面不停地轮询观察,可以比较高性能地查看。

2.2 内存顺序

我们的程序编译器编译时候会对其优化,对于代码的执行顺序会被重新排序。

在多线程编程下,重新排序会给开发带来不可预测的问题,我们不知道编译器怎么排序。

在这样情况下,多线程编程里面有对于内存执行顺序约束或者指定的方法:

  • std::memory_order_relaxed : 程序不对内存执行顺序做保障,不关心内存序
  • std::memory_order_acquire : 一定是和load操作绑定,不能用于store操作。如果非要让store和std::meimory_order_acquire绑定,要么编译器报错,要么此操作是未定义的。std::memory_order_acquire和load操作绑定执行后,所有load操作后面的读写操作都在load操作之后执行完成。原子读之后单向保证。
  • std::memory_order_release : 一定是和store操作绑定,不能用于load操作。如果非要让load和std::meimory_order_release绑定,要么编译器报错,要么此操作是未定义的。std::memory_order_release和store操作绑定执行后,所有store操作前面的读写操作都在store操作之前执行完成。原子写之前单向保证。
  • std::memory_order_acq_rel : 对于原子操作里面既有读操作又有写操作,比如前面提到的compare_exchange_weak/compare_exchange_stronge, fetch_add/fetch_sub操作。这些操作与std::memory_order_acq_rel参数绑定,保证了原子操作之前的读写操作不会排序到原子操作之后,同时保证了原子操作之后的读写操作不会排序到原子操作之前。原子读写操作双向保证。
  • std::memory_order_seq_cst : 默认值,这个保证内存执行顺序和代码保持一致,对于不同的原子变量也是如此。这个是最严格的保证内存序。

2.3 例子

#include <iostream>

std::atomic<bool>  g_ready = false;

int_8 g_result = 0;

void Producer_Proc() {

        g_result = 88;

        g_ready.store(true, std::memory_order_release);

}

void Consumer_Proc() {

        while(!g_ready.load(std::memory_order_acquire)) {

                // sleep 10ms and some other operations

        }

        std::cout << "result: " << g_result << std::endl;

}

int main() {

        std::thread producer(Producer_Proc);

        std::thread consumer(Consumer_Proc);

        producer.join();

        consumer.join();

}

大家看看上面的例子,有两个线程,一个是生产者线程,一个是消费者线程。通过原子变量g_ready的load和store来保证g_result的正确输出。

在线程Producer_Proc里面,原子变量g_ready的store操作与std::memory_order_release绑定,保证了store操作之前的读写操作先执行完成,即g_result = 88操作先完成,这样store操作后,g_ready为true后,也保证了g_result确实更新为88了。

在线程Consumer_Proc里面,原子变量g_ready的load操作与std::memory_order_acquire绑定,保证了load操作之后的读写操作在load操作之后执行。当g_ready更改后,load操作可以获取到其值为true,然后跳出循环,然后输出g_result的操作,即输出g_result的操作一定是在load操作之后执行。

#include <atomic>

class Counter {

       private:

       std::atomic<int> count;

       public:

        Counter() : count(0) {}

        int increment() {

             int expected = count.load();

             while (!count.compare_exchange_weak(expected, expected + 1)) {

               // 循环重试直至成功

             }

             return expected + 1;

       }

             int get() const { return count.load(); }

};

这个程序中运用到了load和compare_exchange_weak操作。

三. 总结

多线程编程中对于内存序重新排序有相应的应对方法,还是要根据具体的情况具体解决。如果对于性能要求不高,第一选择还是用锁的机制。如果对于性能有要求,可以考虑文中用到的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值