概念

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把自己放到等待条件的线程列表上,然后对互斥锁解锁(这两个操作是原子操作)。在函数返回时,互斥量再次被锁住。

悲观锁

互斥锁、自旋锁、读写锁,都是属于悲观锁。

  • 悲观锁认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁
  • 先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

乐观锁

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

实例

多人同时编辑,实际上是用了乐观锁,它允许多个用户打开同一个文档进行编辑,编辑完提交之后才验证修改的内容是否有冲突。常见的 SVNGit 也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的地方,需要我们自己修改后,再重新提交。

使用场景

  • 只有在冲突概率非常,且加锁成本非常的场景时,才考虑使用乐观锁。