当前位置: 首页 > 工具软件 > Spring Sync > 使用案例 >

synchronize-with

盖向荣
2023-12-01

只有真正理解这个概念,才能理解多线程的核心。单个线程的重排可以通过原子操作搞定,但多个线程之间的原子操作没有内在的关系,相当于是两个自治的操作序列,或者说两个局部时间域。如何将这两个时间的某些操作强行规定一个先后关系,而其他操作不管,就是synchronize-with所意味的。

那么如何做同步呢?就是引入一个点,可以成为同步点,这个点是两个线程的参照物,一个线程的一些操作必须在这个点之前完成,另一个线程的操作必须在这个点之后完成。在代码中就是某个操作成为这个同步点,如何让它成为同步点?

就是tagged,打标记。而标记的设计也是成体系的。对应上面说的,memory_order_release的标记就代表前面的操作必须在这个点之前完成,对于其他的操作不管:比如后面的是不是跑到前面完成了。而memory_order_acquire就是这个点后面的操作必须在点后面完成,就刚好禁止了后面的是不是跑到前面完成了这种情况。

synchronize-with

能否翻译一下下面这段出自于《c++ concurrency in action》中关于C++内存模型的描述?

The basic idea is this: a suitably tagged atomic write operation W on a variable x syn-chronizes-with a suitably tagged atomic read operation on x that reads the value stored by either that write ( W ), or a subsequent atomic write operation on x by the same thread that performed the initial write W , or a sequence of atomic read-modify-write operations on x (such as fetch_add() or compare_exchange_weak() ) by any thread, where the value read by the first thread in the sequence is the value written by W(see section 5.3.4).

Back in section 5.3.1, I mentioned that you could get a synchronizes-with relationship between a store to an atomic variable and a load of that atomic variable from another thread, even when there’s a sequence of read-modify-write operations between the store and the load, provided all the operations are suitably tagged. Now that I’ve covered the possible memory-ordering “tags,” I can elaborate on this. If the store istagged with memory_order_release, memory_order_acq_rel, or memory_order_seq_cst, and the load is tagged with memory_order_consume, memory_order_acquire,or memory_order_seq_cst, and each operation in the chain loads the value written by the previous operation, then the chain of operations constitutes a release sequence and the initial store synchronizes-with (for memory_order_acquire or memory_order_seq_cst) or is dependency-ordered-before (for memory_order_consume) the final load. Any atomic read-modify-write operations in the chain can have any memory ordering (even memory_order_relaxed).

上面那段话的基本意思是:如果有一个线程A对一个atomic对象调用了write操作,假设使用的是std::memory_order_release,而线程B对这个atomic对象调用了read操作,假设使用的是std::memory_order_acquire,那么即使线程A在之前的那个write之后,在线程B的read之前有一个使用std::memory_order_relaxed的写操作,也不会影响没有这个write操作建立的同步(synchronize-with)关系,而且线程B读到的是线程A第二次write的值。如果期间存在线程对这个atomic对象调用读写操作,那么线程A的写操作(使用std::memory_order_release)与线程B的读取操作(使用std::memory_order_acquire)仍然存在同步(synchronize-with)关系,而且中间的读写操作读取的是线程A写的值,线程B读取的是中间读写操作写的值,即使中间读写操作使用的std::memory_order_relaxed。当然,写操作和读操作不是一定需要用上面说的两种内存序(memory_order),可以选择其他能够建立同步(synchronize-with)关系的内存序。

比如consume就是dependency-ordered-before。

section5.3.1解释的是synchronizes-with这个概念,我的理解就是解释两个CPU操作(单线程内或者多线程内)在什么情况下算是“同步”的。首先要知道程序在编译、执行过程中可能发生reorde——即指令重排。CPU实际的指令执行顺序称为“memory ordering”。在memory ordering中,有相邻的两个操作write x/read x(当然,中间也可以有对其他变量的操作),那么,read到的x一定是write到的值,这就是“同步”。你要问,这不是很显然的吗?请搜索“CPU高速缓存一致性”的相关介绍,简单来说,如果没有CPU缓存一致性,上述两个操作就可能得不到正确的结果。内存模型均以CPU缓存一致性模型为基础,所以在介绍C++内存模型之前,作者先简要介绍了一下CPU操作的同步这个概念。

大致翻译:无论在何种指令重排模式(强一致性、弱一致性)下,程序中(programming order)对x变量的write或者后续对x的写或者其他线程对x的写,或者把写改成RMW,在实际的CPU执行顺序中(memory order)对x的读到的值就是刚刚对x的写入的值。

要准确理解这个,首先需要看synchronizes-with以及happens-before的定义。对于一般的原子操作,如果他们直接没有任何上述关系,那么优化器可以方便地把该变量缓存到诸如寄存器什么的地方,只要保证读取和写入的原子性(从上层调用者看来)即可。这就是relaxed的语义。

如果要建立synchronizes-with关系,必须确保对该原子变量的写操作对于所有建立有synchronizes-with关系的线程都可见。简单粗暴的做法是强制的让所有的线程(无论有无synchronizes-with关系)都可见,这就是“序列一致”语义。

序列一致简单是简单,但是要求太严格。因为我们只想在相关的线程上保持序列一致即可。要满足这个条件,一个简单的办法是让相关的线程给这个原子变量打标记,告诉编译器说,注意,针对这个变量的写操作,必须保证所有的打了标记的线程的读操作都能立即看到。这就是acquire/release语义。

内存模型还是《Java Concurrency in Practice》讲得比较易懂。

建议读C++ Concurrency in Action第五章。作者是线程库的提议者和实现者。但是这个深度未必能达到你的期望,因为这些非序列一致的内存序都是为专家准备的,一般不会深入讲解。

 类似资料: