Linux 常见的锁

我们在开发中使用的几种常见的锁主要有互斥锁、自旋锁、读写锁、乐观锁和悲观锁这五种。

锁的用途

  • 互斥锁是一个互斥的同步对象,意味着同一时间有且仅有一个线程可以获取它,互斥锁可适用于一个共享资源每次只能被一个线程访问的情况
  • 自旋锁适用于:短暂的访问临界区时适用 – 达到减少切换消耗
  • 读写锁适用于:用于解决读操作较多的场景 – 减少锁等待提高并发性。

接口

man pthread.h

关键字:
互斥锁:pthread_mutex_t
自旋锁:pthread_spinlock_t
读写锁:pthread_rwlock_t

1、互斥锁

互斥锁加锁失败后,线程会释放CPU,给其他进程使用。
自旋锁加锁失败后,线程会忙等待,知道它拿到锁。

互斥锁是一种独占锁,比如当线程A加锁成功后,此时互斥锁已经被线程A独占了,只要线程A没有释放手中的锁,线程B加锁就会失败,于是就会释放CPU给其他线程,既然线程B释放了CPU,自然线程B加锁的代码就会被阻塞。

对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为睡眠状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行了。

所以,互斥锁加锁失败时,会从用户态陷入到内核态,让内核帮我们完成切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本,即有两次线程上下文切换:
当线程加锁失败时,内核会把线程的状态从运行状态设置为睡眠状态,然后把CPU切换给其他进程运行;
当锁被释放时,之前睡眠状态的进程会变为就绪状态,然后内核会在合适的时候,把CPU切换给该线程运行。

如果你锁住的代码执行时间比较短,那可能上下文切换的时间比你锁住的代码执行时间还要长。

所以,如果你能确定被锁住的代码执行时间很短,就不应该使用互斥锁,而应该使用自旋锁,否则使用互斥锁。

2、自旋锁

自旋锁是通过CPU提供的CAS函数,在用户态完成加锁和解锁的操作,不会主动产生上下文的切换,所以相比互斥锁来说,会快一些,开销也小一些。
使用自旋锁的时候,当发生多线程竞争锁的情况,加锁失败的线程会忙等待,直到它拿到锁。所以,在单核CPU上,需要抢占式的调度器。否则,自旋锁在单CPU上无法使用,因为一个自旋的线程永远不会放弃CPU。

自旋锁开销少,在多核系统下一般不会产生线程切换,适合异步、协程等在用户态切换请求的编程方式,但如果被锁住的代码执行时间过长,自旋的线程会长时间占用CPU资源。

自旋锁与互斥锁使用层面比较类似,但实现层面完全不同:当加锁失败时,互斥锁用线程切换来应对,自旋锁则用忙等待来应对。

它们俩是锁的最基本处理方式,更高级的锁都会选择其中一个来实现,比如读写锁既可以选择基于互斥锁实现,也可以选择基于自旋锁实现。

3、读写锁

读写锁由读锁和写锁两部分构成,如果只读取共享资源用读锁加锁,如果要修改共享资源则用写锁加锁。所以,读写锁适用于能明确区分读操作和写操作的场景。

读写锁工作原理:

——当写锁没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为读锁是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源。

——但是,一旦写锁被线程持有后,读线程的获取锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。

所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。所以,读写锁在读多写少的场景,能发挥出优势。

另外,根据实现的不同,读写锁可以分为读优先锁和写优先锁。

读优先锁对于读线程并发性更好,但是如果一直有读线程获取读锁,那么写线程将永远获取不到写锁,这就造成了写线程饥饿的现象。

写优先锁可以保证写线程不会饿死,但是如果一直有写线程获取写锁,读线程也会被饿死。

还有一种公平读写锁:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现饥饿的现象。

4、乐观锁与悲观锁

前面提到的互斥锁、自旋锁、读写锁,都属于悲观锁。

悲观锁认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。

而乐观锁认为冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

乐观锁全程没有加锁,所以它也叫无锁编程。

乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。

你可能感兴趣的:(Linux,linux)