自旋锁与互斥锁

互斥锁(mutex)

当一个线程试图锁定一个互斥锁而没有成功时,由于该互斥锁已经被锁定,它将进入睡眠状态,并立即允许另一个线程运行,它将会立即睡眠直到被唤醒,当它睡眠前的上锁的线程解锁时,多个沉睡的线程会竞争锁,得到锁的话会被唤醒.

自旋锁(spin-lock)

当一个线程使用自旋锁锁定没有成功的时候,它将不断的尝试锁定,直到锁定成功;因此,自旋锁不允许其他线程代替它,但是,一旦当前线程的被分配的时间片被用完,操作系统才可能会强迫选择其他的线程,即调度.

如何选择

选择的重点就只有一个问题,在互斥锁中使一个线程睡眠然后再唤醒,这一个操作是十分昂贵的,这将需要相当多的CPU指令,并因此花费很多时间,如果当前的互斥锁仅锁定很短的时间,那么花费在是一个线程沉睡然后唤醒的的时间可能超过这个线程真实沉睡的时间,甚至也超过一个线程通过自旋锁不断的轮询所花费的时间,从另一方面来说,自旋锁的轮询将会不断的浪费CPU的时间,如果这个锁保持了相当长的时间的话,这将会浪费大量的CPU时间,如果这时线程沉睡的话就好了,所以我们可以看到,其实两种锁的使用完全是看场景的.

其实还有一个问题,就是分别在单核机器与多核机器上,两种锁也有很大的差别,其实也不能说差别,就是在单核机器上跑自旋锁是没有什么意义的,因为自旋锁轮询占有了唯一有效的CPU,没有其他线程可以运行,这个锁不会被解锁.只会浪费CPU上的时间而没有任何好处,如果换为线程去沉睡,其他线程就可以立刻运行了,如果解锁的话,就会在第一个线程被唤醒后允许第一个线程继续执行.
但是在多核机器上,如果在很短的时间内拥有大量锁的话,花费在不断使得线程沉睡和唤醒的时间就将会显著的降低运行时消耗,想想看一个处理http请求的webserver,请求每秒高达几十万次,线程之间对于任务队列的的请求都会涉及到一个沉睡和唤醒,这是多么大的开销啊.会到上面的问题,如果我们使用自旋锁替代的话,线程将会有机会运用其完整的运行时间(用于经常只锁定一个很短的时间周期,随后立即唤醒),但是会导致很高的处理量.
事实上,很多现代的操作系统拥有混合互斥锁与混合自旋锁,这是什么意思呢?

一个混合的互斥锁的行为才开始时很像自旋锁,如果线程无法锁定这个互斥元,它就会被锁定,即进入沉睡,因为互斥锁很可能很快被解锁,所以替换为互斥锁开始的行为类似于自旋锁,只有在一段确定的时间(或重试或者其他任何的衡量因素)后仍未获得锁,线程仍将会睡眠,如果相同的代码在单核机器上运行,锁就不会再是自旋锁,如上所述,那没有睡眠好处.

一个混合的自旋锁开始就是一个普通的自旋锁,但是为了避免浪费太多的CPU时间,它可能会有一种后退策略.首先它不会使线程去沉睡(你在使用自旋锁的时候不希望发生这种事情),但是这个线程可能会被停止(立即或是一段时间后),而且允许其他的线程继续执行,因此增加了自旋锁被解锁的几率还提升了并发性(一个纯的线程切换一般比使一个线程沉睡随后唤醒它要廉价(虽然差别不大)).

总结

如果疑惑的话就使用互斥锁吧,它通常是个不错的选择而且大多数现代操作系统允许它在短时间内自旋,使用自旋锁确实可以在有些时候提升性能,但是确实是很少的时候,其实我们可以实现一种属于自己的锁对象,在其内部我们可以使用自旋锁或者互斥锁,行为我们是可以修改的,我们在上面提到了先在一段时间内自旋,没有得到锁就睡眠的混合锁,在C++标准库中提供了这样一个工具,即std::timed_mutex,简单介绍下这个工具
在这里插入图片描述
我们可以看到其有一个try_lock_for和一个try_lock_until的函数,区别就是一个等待时间段,一个等待时间点,我们可以

std::timed_mutex Mutex;
chrono::milliseconds timeout(100);
if (Mutex.try_lock_for(timeout)){
	//正常获取锁之后的代码
}
else{
	//锁定
}

这样就实现了一个简单的混合锁,最重要的地方在于自旋的时长,这需要针对与不同的场景自己测试出来一个较优的结果.

参考:
https://stackoverflow.com/questions/5869825/when-should-one-use-a-spinlock-instead-of-mutex

https://blog.csdn.net/swordmanwk/article/details/6819457

https://en.cppreference.com/w/cpp/thread/timed_mutex

<<C++ Concurrency in action>>

posted @ 2022-07-02 13:18  李兆龙的博客  阅读(111)  评论(0编辑  收藏  举报