CherieLi Student

锁优化

2019-08-30
CherieLi

互斥锁与自旋锁

互斥锁 是阻塞锁,当某线程无法获取互斥锁时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放互斥锁后,操作系统会激活那个被挂起的线程,让其投入运行。

自旋锁 是一种非阻塞锁,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。

std::mutex临界区较小,可考虑使用更轻量级的锁,例如spin lock; 使用tbb::spin_mutex代替std::mutex

std::mutex用于保护std::unordered_map,可考虑使用lock free map替代原本的std::unordered_map。

spinlock/mutex
轻量级互斥锁,用户不可见,适用于多线程间简单共享数据结构的修改保护,只有exclusive(独占)模式。一般基于CAS类的原子操作实现,当ptr的值为0时swap为1,加锁成功,如果ptr的值为1,则swap失败,加锁不成功,继续等待重试。

两种锁适用于不同场景:

1.如果是多核处理器,如果预计线程等待锁的时间很短,使用自旋锁是划算的。

2.如果是多核处理器,如果预计线程等待锁的时间较长,建议使用互斥锁。

3.如果是单核处理器,一般建议不要使用自旋锁。因为,在同一时间只有一个线程是处在运行状态,那如果运行线程发现无法获取锁,只能等待解锁,但因为自身不挂起,所以那个获取到锁的线程没有办法进入运行状态,只能等待运行线程把操作系统分给它的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。

4.如果加锁的代码经常被调用,但竞争情况很少发生时,应该优先考虑使用自旋锁,自旋锁的开销比较小,互斥锁的开销较大。

共享锁(读锁)
共享锁是指该锁可被多个线程所持有。

悲观锁
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

乐观锁
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁(使用队列进行排序,FIFO)

非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。(概率上会造成优先级反转或者饥饿现象)

阻塞锁
阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间)时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。阻塞锁的优势在于,阻塞的线程不会占用cpu时间,不会导致cpu占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。 在竞争激烈的情况下,阻塞锁的性能要明显高于自旋锁。 理想的情况:在线程竞争不激烈下使用自旋锁,竞争激烈下使用阻塞锁。

无锁队列
https://www.cnblogs.com/catch/p/5129586.html

CAS操作
type __sync_val_compare_and_swap(type *ptr, type oldval type newval, …)
比较ptr与oldval的值,如果两者相等,则将newval更新到ptr并返回操作之前*ptr的值。
http://wfeii.com/2021/08/07/atomic.html

FADD操作
type __sync_fetch_and_add (type *ptr, type value); 函数提供原子加并返回原来ptr的值。

Latch/RWLock
轻量级读写锁,用户不可见,其加锁的范围相对于spinlock来说范围更进一步扩大,适用于临界区较大且具有复杂的逻辑处理流程,更适合读多写少的场景)。加锁对象通常类似Btree index等结构体。锁模式分为shared(共享)和exclusive(独占)两种模式。

Innodb transaction lock
https://zhuanlan.zhihu.com/p/493415374

MySQL MDL lock
https://zhuanlan.zhihu.com/p/130318750

锁性能优化

1.无锁编程:在多线程竞争激烈的情况下,使用无锁算法的整体吞吐量会优于加锁的算法,因为它避免了调度延时和频繁的上下文切换。如:单生产者单消费者的无锁队列。
2.大锁变小锁:并发任务高的场景下,如果系统中存在唯一的全局变量,那么每个CPU core都会申请这个全局变量对应的锁,导致这个锁的争抢严重。可以基于业务逻辑,为每个CPU core或者线程分配对应的资源。
3.使用gcc自带的原子操作:推荐使用GCC(7.3.0)实现的atomic系列代码,跨平台移植性好,性能也非常好。
4.使用自带内存屏障的指令:推荐使用ldaxr/stlxr指令实现锁或原子操作,替换ldxr/stxr +内存屏障(dmb)的实现。
5.调整线程数达到最佳并发效果:多线程可以提升系统吞吐量,但是却会增加锁的争抢,更会增加线程切换带来的CPU损耗。
6.避免cache line伪共享:通过数据结构优化或字节对齐,将频繁读和频繁写的数据放入不同Cache line,减少“锁”数据结构的伪共享。

spinlock

spinlock是个while循环,会导致cpu使用率变高,一般仅用在短暂访问临界区场景。


上一篇 gdb调试方法

下一篇 rpc 学习

Comments

Content