一步一步写线程之十memory_order的应用

222 篇文章 92 订阅

一、并发中的数据处理

在前面已经多次分析过并发及并行编程中的数据处理,特别是同步处理。如果跳出来看这个数据同步的问题,开发者可以看到,在上层使用一些库或者 OS系统提供的同步机制来实现多线程之间数据操作的并发安全。可进一步分析呢?在这些所以的基础调用中,是如何实现的数据同步呢?可以看一下库或者操作系统的源码,比如glibc中使用futex实现。如此抽丝剥茧的分析下去,总会查找到最下面的同步处理机制。
等到找到源头时,才会发现,其实很多都是规则或者说标准,也可以说是协议,当然称做其它也无不可。回到同步,数据的同步其实就是数据处理的规则。而所谓的数据同步其实就是CPU和内存之间的数据的安全传递(后来又出现了多核心和多CPU,这就又涉及到不同CPU间的数据通信)。那么这就涉及到了,内存中如何处理数据。而数据的处理要么按块,要么按流,要么按最小的单元也就是常提到的原子操作。而如果具体到数据的同步,最终就会体现在原子操作上,而对原子操作的同步,就不可避免的需要定义一下处理的规则,这个规则是什么?就是处理原子操作的优先过程,也可以认为是处理的顺序。
举一个简单的例子,军训中的报数,其实就是一个严格次序的过程,一定是从头到尾依次进行,中间不能漏掉也不能任意插入其它人的报数。但是,多个班的报数就可以非严格的按次序进行,只要最终报道都达到约定数量即可。
如果把CPU处理内存数据比做处理报号的过程,其实就可以根据不同的情况来使用不同的调用处理顺序来完成。这在C++中称为memory_order.

二、std::memory_order

在cppreference上英文是这样描述的:

std::memory_order specifies how memory accesses, including regular, non-atomic memory accesses, are to be ordered around an atomic operation. Absent any constraints on a multi-core system, when multiple threads simultaneously read and write to several variables, one thread can observe the values change in an order different from the order another thread wrote them. Indeed, the apparent order of changes can even differ among multiple reader threads. Some similar effects can occur even on uniprocessor systems due to compiler transformations allowed by the memory model.

The default behavior of all atomic operations in the library provides for sequentially consistent ordering (see discussion below). That default can hurt performance, but the library's atomic operations can be given an additional std::memory_order argument to specify the exact constraints, beyond atomicity, that the compiler and processor must enforce for that operation.
Inter-thread synchronization and memory ordering determine how evaluations and side effects of expressions are ordered between different threads of execution. They are defined in the following terms:

说的简单一些,就是std::memory_order和同步是确定线程间按顺序求值的条件。它主要分成以下几种类型:

1、memory_order_relaxed
Relaxed operation: there are no synchronization or ordering constraints imposed on other reads or writes, only this operation’s atomicity is guaranteed (see Relaxed ordering below).
宽松内存序,无法保证操作的顺序,只保证原子操作的完整性。
2、memory_order_consume
A load operation with this memory order performs a consume operation on the affected memory location: no reads or writes in the current thread dependent on the value currently loaded can be reordered before this load. Writes to data-dependent variables in other threads that release the same atomic variable are visible in the current thread. On most platforms, this affects compiler optimizations only (see Release-Consume ordering below).

消费内存序(和memory_order_release一起称为释放-消费内存序),它其实就是当前线程的操作(一般是读)依赖于它的前一个写操作。在大多数平台上,这只影响到编译器优化。memory_order_consume 比 memory_order_acquire要稍弱一些,而且基本不推荐使用它。
3、memory_order_acquire
A load operation with this memory order performs the acquire operation on the affected memory location: no reads or writes in the current thread can be reordered before this load. All writes in other threads that release the same atomic variable are visible in the current thread (see Release-Acquire ordering below).
获取内存序(和memory_order_release一起称为获取-释放内存序),即当前线程所有的后续操作必须在当前操作完成后才可以进行。
4、memory_order_release
A store operation with this memory order performs the release operation: no reads or writes in the current thread can be reordered after this store. All writes in the current thread are visible in other threads that acquire the same atomic variable (see Release-Acquire ordering below) and writes that carry a dependency into the atomic variable become visible in other threads that consume the same atomic (see Release-Consume ordering below).
释放内存序(和上面两类内存序一起工作 ),即能够保证当前线程中所有的操作完成后才可以执行此操作。
5、memory_order_acq_rel
A read-modify-write operation with this memory order is both an acquire operation and a release operation. No memory reads or writes in the current thread can be reordered before the load, nor after the store. All writes in other threads that release the same atomic variable are visible before the modification and the modification is visible in other threads that acquire the same atomic variable.
它是memory_order_release和memory_order_acquire的组合,即获取-释放内存序。
6、memory_order_seq_cst
A load operation with this memory order performs an acquire operation, a store performs a release operation, and read-modify-write performs both an acquire operation and a release operation, plus a single total order exists in which all threads observe all modifications in the same order (see Sequentially-consistent ordering below).
顺序一致性内存序,最严格的内存序,需要全局保持操作顺序的一致性。在C++的原子库中默认使用的这种内存序,所以,其对执行效率有一定的影响。在一些开源的框架,如Redis中,就可以发现,在某些情况下使用了不同的内存序。

memory_order_acquire和memory_order_consume的不同在于,前者保证对协同使用的 memory_order_release操作之前的所有原子和非原子的写入操作有效,后者只保证协同使用的 memory_order_release操作之前的所有原子的写入操作和依赖原子写入的非原子操作有效。
在X86平台上,基本上这些内存序的处理非常简单,就是几条指令的事。在对memory_order_seq_cst支持中,都有专门的指令,但出于效率考虑,CPU硬件上一般不会使用这种内存序。内存序对编译优化中的指令重排(还有多CPU乱序执行)和CAS(compare_exchange_strong)无锁编程中,是相当关键的。因为它直接决定了使用哪种机制来处理数据。大家可以看下CAS中提供的compare_exchange_strong等操作,它内部就有对内存序的处理参数。

三、原理分析和说明

在计算机中,数据的同步处理其实主要分成两个部分,即数据的交换(Cache和CPU,Cache和Memory,CPU和CPU)和指令重排。这又涉及到两部分,硬件中的内存序和编程语言的内存序,而本次主要说的其实是后者。而前者则和不同的厂家生产的CPU的相关指令有关,如果有兴趣,可以自行查阅Intel、AMD以及ARM相关架构的指令手册。
其实内存序的目的就是为了兼顾效率和安全,严格的使用全局顺序一致,也就是串行一致,则一定会在大多数场景下影响效率。但是这个效率受影响的度,和实际的场景又有关系,所以开发语言对外提供了几种不同的内存序,目的当然是让开发者在知道自己的应用场景下,更好的在安全的前提下,提高处理的效率。
包括前面的指令重排,在多线程的优化中就非常重要。而数据的交换,又必须保证安全,否则交换的意义何在?而通过同步再加上内存序就可以更好的处理一些原子和非原子的操作,从而在底层提供了平衡二者的机制。
另外,一般在开发过程中,有开发者喜欢使用volatile来处理缓存机制导致的一些意外。但在内存序中,是无法保证多线程中这种访问是安全的,或者是原子的,固定排序的。不过,VC是个例外,它可以保证这种顺序,所以其可以在多线程中通过volatile关键字来实现数据的同步。

四、例程

#include <atomic>
#include <cassert>
#include <thread>

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); // will never happen
}

其实上面的代码如果换为memory_order_relaxed在ARM架构上还是有可能触发断言的,但在X86的机器上不会。大家可以用多线程的思想和执行的顺序进行思考,即可明白。

五、总结

通过上述的分析和例程测试,可以清理的明白内存模型中内存顺序的意义。通过定义指定的内存序,可以防止编译器的一些特定的操作(指令重排和可见性等),也可以理解为,从标准上屏蔽了对编译器的额外的处理,使得数据在不同平台的不同线程中,只在语言层面就可以控制内存的数据交互。
控制层面向高层发展的结果就是,降低了开发者和设计者的负担,能够更安全的进行多线程的数据编程。

  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值