lock-free 和 wait-free 基本概念与相关知识点
https://blog.csdn.net/misayaaaaa/article/details/100063319 1、为什么会提出无锁化的问题?前提:多核编程有冲突,多个线程修改同一个数据会造成race condition。所以需要用锁。问题:锁容易造成性能瓶颈—> 如何无锁化—> 原子操作 2、原子操作可能带来的问题(1)这个操作可能并没有那么快
https://blog.csdn.net/misayaaaaa/article/details/100063319
1、为什么会提出无锁化的问题?
前提:多核编程有冲突,多个线程修改同一个数据会造成race condition。
所以需要用锁。
问题:锁容易造成性能瓶颈—> 如何无锁化—> 原子操作
2、原子操作可能带来的问题
(1)这个操作可能并没有那么快 —> 无法解决锁带来的性能瓶颈问题
(2)有可能会造成程序的crash
3、Cacheline
【先了解背景知识,明白问题2是怎么造成的】
(1)现代CPU为了提高性能,大量使用分级Cache,L1和L2为核心独享,L3为核心间共享。
(2)写入自身L1-Cache非常快,2ns完成;但是如果有其他核心也写同一处内存,需要确认其他核心的cacheline,这个过程是原子的,大概需要700ns,一致性同步。
因此,耗时的瓶颈就在CPU同步cacheline。
【可以理解为多核同写一处地址,这两个核的操作就没法并行去走了,需要确定一个顺序将两个核要执行的指令依次排列下来,这样就极大影响了性能】
4、如何尽可能避免CPU的同步cache
(1)最好的做法:多线程间尽量避免共享内存
race condition总是麻烦的,如果从源头解决,那才是极好的。
多线程的变量尽量按访问规律排列;
5、为什么会crash
没有依赖,没有锁控制,由于指令重排,可能代码后面的指令会跑到代码前面去。
-
``
`c++
-
// Thread 1
-
// ready was initialized to false
-
p.init();
-
ready = true;
-
`
``
-
-
``
`c++
-
// Thread2
-
if (ready) {
-
p.bar();
-
}
-
`
``
指令的重排序可能会导致ready=true写到p.init()的前面,导致线程2的执行出现问题。
因此,有了memory order的概念,将指令操作进行了抽象,总结了几种模式:
memory_order_relaxed:放任自由,编译器爱怎么搞怎么搞
memory_order_consume:后面依赖此原子变量的访问指令勿排到此条指令之前
memory_order_acquire:后面访存指令重排至此指令之前
memory_order_release:前面访存指令勿重排至此指令之后,当此指令的结果对其他线程可见后,之前所有的指令都可见。
memory_order_acq_rel:acquire+release
memory_order_seq_cst:
这些似乎很难理解,确实结合例子来看会舒服一些:
-
```c++
-
// Thread1
-
// ready was initialized to false
-
p.
init();
-
ready.
store(
true, std::memory_order_release);
-
```
-
-
```c++
-
// Thread2
-
if (ready.
load(std::memory_order_acquire)) {
-
p.
bar();
-
}
-
```
(1)ready的赋值操作采用了原子操作,并指明采用release,即:前面的指令不能排到ready赋值之前。
(2)ready的判断采用了原子操作,并指明采用acquire,即:前面访存的指令不能放在这条指令的后面,这样保证了ready=true和if ready判断的先后顺序
6、wait-free & lock-free
(1)wait-free:不管OS如何调度线程,每个线程始终在做有用的事情。
(2)lock-free:不管OS如何调度线程,至少有一个线程在做有用的事情。
因此,如果服务中有了锁,有可能拿到锁的线程去做IO,等待;其他线程又依赖这个锁,整个线程没有做有用的事情,因此有锁一定不是lock-free,更不可能是wait-free。
7、纠正悖论
使用lock-free或者wait-free并不一定会使性能加快,但是能保证一件事情总能在确定的时间完成。
why?
(1)race-condition和aba问题比用锁更复杂;
(2)使用锁会是race发生时,尝试另一个途径避免竞争,高度竞争的时候规避了cacheline同步,能让拿到锁的线程很快独占完成流程。减少不必要的上下文开销。
使用锁导致低性能往往有两种原因:
(1)临界区过大,导致并发度下降;
(2)临界区过小,使用锁上下文切换占据了更多的耗时。
面对第一种情况,即便是采用无锁,需要写复杂的代码让并发的线程执行串行化,反而会增加了多核之间互相跳转,降低Cache的命中率,增加开销;
面对第二种情况,这时候采用原子指令会增加速度,因为减去了占去大头的上下文切换耗时。
8、概念
内存模型(指令重排)
实际上,内存模型是一个比较泛的概念,通常是硬件上的概念:表示机器指令(汇编指令)以什么样的顺序执行被处理器执行。值得注意的是,出于性能和效率上的考虑,现代处理器并不总是逐条执行机器指令的,可以简单理解为:处理器的执行顺序并不总是和代码顺序一致。如此也就导致了在某些情况下,程序的运行结果并不是所期待的结果。如:
-
int a;
-
int b;
-
void Func() {
-
int t =
1;
-
a = t;
-
b =
2;
-
}
上面的代码被编译为机器指令之后可能会是这样:
-
1. load reg3 ,
1;
-
2. mov reg4, reg3;
-
3. store reg4,
a;
-
4. load reg5,
2;
-
5. store reg5,
b;
大部分时候,上面的伪汇编代码按照“1>2>3>4>5”这样的顺序执行,但是从上面的代码中可以看到:指令1、2、3和指令4、5在运行顺序上毫无影响,在某些CPU上可能会按照“1>4>2>5>3”这样的顺序执行,这就是所谓的指令重排。在足够复杂的情况下,指令重排会导致无法得到正确的结果。
9、总结
(1)无锁化并不一定能带来高性能,但一定能保证一件事情在确定的时间内完成;
(2)使用无锁化会带来两个问题:性能和crash;
(3)面对无锁化使用的性能问题:采用规避原则,尽可能多核少共享内存资源,少同时操作一个资源;
(4)面对无锁化crash问题,分析了原因是指令重排序,引入了memory_order,制定相应的模型规定指令的执行先后顺序,将多核指令cacheline。
(5)临界区较大一定上锁,小临界区尽可能用原子指令。
更多推荐
所有评论(0)