C++ lock-free 和 wait-free 基本概念与相关知识点

 

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)临界区较大一定上锁,小临界区尽可能用原子指令。

 

看了很多博客,发现讲的都不是特别清楚,如果有读者有反对意见,欢迎讨论!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MISAYAONE

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值