概念
Linux
系统提供了五种用于线程通信的方式:
- 互斥锁
- 读写锁
- 条件变量
- 自旋锁
- 信号量
互斥锁(mutex
)
互斥锁(
Mutex
)是一种用于线程同步的机制,用于保护共享资源在多个线程间的互斥访问。
互斥原理
当一个线程获得互斥锁时,其他线程需要等待该线程释放锁才能继续执行。 通过互斥锁的加锁和解锁操作,可以确保同一时间只有一个线程能够访问共享资源,从而保证数据的一致性和正确性。
机制
- 加锁操作:当线程想要访问共享资源时,首先需要尝试获得互斥锁。如果互斥锁已被其他线程占用,则当前线程会被阻塞,直到互斥锁被释放。一旦线程成功获得互斥锁,它就可以安全地访问共享资源。
- 解锁操作:线程使用完共享资源后,应该释放互斥锁,以便其他线程可以继续访问。释放互斥锁将导致等待该锁的线程中的一个或多个线程恢复执行。
互斥锁与自旋锁的区别
加锁失败
- 互斥锁使用线程切换应对,把当前线程放入到锁的等待队列,释放
CPU
。 - 自旋锁用忙等待应对。
互斥锁与读写锁(互斥)的区别
- 互斥锁(
Mutex
):互斥锁保证了在任意时刻只有一个线程能够获得锁。当一个线程获得互斥锁后,其他线程必须等待该线程释放锁才能继续执行。互斥锁适用于对共享资源的互斥访问,既包括读操作也包括写操作。 - 读写锁(
ReadWrite Lock
):读写锁允许多个线程同时读共享资源,但在写共享资源时需要互斥访问。多个线程可以同时获取读写锁的读锁,只有当没有线程持有读锁时,写锁才能被获取。读写锁适用于读多写少的场景,可以提高并发性能。
自旋锁(Spinlock)
通过
CPU
提供的CAS
函数(Compare And Swap
),在用户态完成加锁和解锁操作,不会主动产生线程上下文切换
- 在单核
CPU
上,需要抢占式的调度器,因为一个自旋的线程永远不会放弃CPU
。 - 自旋锁初始化为
1
。
上锁过程
- 第一步,查看锁的状态,如果锁是空闲的(
lock-1== 0
),则执行第二步操作; - 第二步,将锁设置为当前线程持有;使用自旋锁的时候,当发生多线程竞争锁的情况,加锁失败的线程的忙等待,(不断比较
lock is = 1
)。 CAS
函把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。这里的「忙等待」可以用while
循环等待实现,不过最好是使用CPU
提供的PAUSE
指令来实现「忙等待」,因为可以减少循环等待时的耗电量。
实现
汇编实现:
C语言模拟:
void spin_lock(amtoic_t *lock)
{
again:
result = --(*lock);
if (result == 0) {
return;
}
while (true) {
if (*lock == 1) {
goto again;
}
}
}
自旋锁使用场景
- 操作系统内核的并发数据结构(短临界区)
- 许多操作系统内核对象具有
read-mostly
特点 - 多处理器的临界资源保护(关中断只能实现单个处理器的临界区保护)
读写锁(Read-Write Locks
)
适用于能明确区分读操作和写操作的场景。
- 读写锁在读多写少的场景,能发挥出优势。
- 写锁是独占锁,会阻塞其他写和读操作。
- 读锁是共享锁,在写锁未被持有时,多个线程可以并发持有读锁,大大提高了共享资源的访问效率。
- 公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。
条件变量
在多线程程序中用来实现等待 -> 唤醒逻辑常用的方法。
利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
- 一个线程等待”条件变量的条件成立”而挂起; 另一个线程使“条件成立”。
- 为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。线程在改变条件状态前必须首先锁住互斥量,函数
pthread_cond_wait
把自己放到等待条件的线程列表上,然后对互斥锁解锁(这两个操作是原子操作)。在函数返回时,互斥量再次被锁住。
悲观锁
互斥锁、自旋锁、读写锁,都是属于悲观锁。
- 悲观锁认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
- 先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
乐观锁
乐观锁全程并没有加锁,所以它也叫无锁编程。
实例
多人同时编辑,实际上是用了乐观锁,它允许多个用户打开同一个文档进行编辑,编辑完提交之后才验证修改的内容是否有冲突。常见的 SVN
和 Git
也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的地方,需要我们自己修改后,再重新提交。
使用场景
- 只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。