星期六 2021-07-31
最近在完成课程的相关实验,涉及到有关锁和不同类型的同步对象,所以我写下来这篇文章,记录并学习这方面的知识。
Locks
锁是一个抽象的概念。基本前提是锁保护对某种共享资源的访问。如果你拥有一个锁,那么您可以访问受保护的共享资源。如果你不拥有该锁,则无法访问共享资源。
要拥有一个锁,首先需要某种可锁定的对象。然后从该对象获取锁。精确的术语可能会有所不同。例如,如果你有一个可锁对象 Duck,你可以表示为下列术语:
- 获取 Duck 上的锁
- 锁定 Duck
- 获得 Duck 的所有权
- 或者一些特定于 Duck 类型的类似术语
锁的概念还意味着一种排除性: 有时你可能无法获得锁的所有权,这样做的操作要么失败,要么被阻塞。在前一种情况下,操作将返回一些错误代码或异常,以表明取得所有权的尝试失败。在后一种情况下,操作在获得所有权之前不会返回,这通常要求系统中的其它线程做相关操作来允许这种情况发生。
最常见的排除方式是使用简单的数字计数: 可锁对象具有所有者的最大数量。如果这个已经达到这个数量,那么任何尝试获取锁定此对象将无法成功。因此,这就要求我们有某种机制,在我们操作完成后放弃此对象的所有权。这通常被称为解锁,但同样的术语可能会有所不同。例如下列术语:
- 释放 Duck 上的锁
- 放下 Duck 上的锁
- 解锁 Duck
- 放弃 Duck 的所有权
- 或者一些特定于 Duck 类型的类似术语
当你以适当的方式放弃所有权时,如果满足了所需的条件,则尝试获取锁的阻塞操作便会停止阻塞。例如,如果一个可锁定的对象只允许3个拥有者,那么第4次尝试获得锁被阻止。当前3个拥有者中的一个释放了锁,那么第4个尝试获取锁将成功。
Ownership
“拥有”一把锁的含义取决于可锁定对象的精确类型。对于一些可锁对象,所有权的定义非常严格: 这个特定的线程通过使用这个特定的对象,在这个特定的范围内拥有锁。
在其他情况下,定义更具流动性,锁的所有权更具概念性。在这些情况下,可以由不同于获得锁的线程或对象的线程或对象来释放所有权。
Mutexes
互斥是互斥锁的缩写。除非该单词使用其他术语(如共享互斥、递归互斥或读/写互斥)进行限定,否则它引用的是一种可锁对象,每次只能由一个线程拥有。只有获取锁的线程才能释放互斥锁。当互斥锁被锁定时,任何获取锁的尝试都将失败或阻塞,即使这一尝试是由同一个线程完成的。
Recursive Mutexes
递归互斥类似于普通互斥,但是一个线程可能同时拥有多个锁。如果线程 a 获得了递归互斥锁,那么线程 a 可以获得递归互斥锁的进一步锁,而无需释放已持有的锁。但是,在线程 a 持有的所有锁被释放之前,线程 b 不能获得递归互斥锁上的任何锁。
在大多数情况下,递归互斥体是不受欢迎的,因为它使正确推理代码变得更加困难。对于普通互斥体,如果您在释放所有权之前确保受保护资源上的不变量是有效的,那么您就知道,当您获得所有权时,这些不变量是有效的。
对于递归互斥锁,情况并非如此,因为能够获取锁并不意味着当前线程尚未持有锁,因此并不意味着不变量有效。
Reader/Writer Mutexes
有时被称为共享互斥、多读/单写互斥或只是读/写互斥,它们提供两种不同的所有权类型:
- 共享所有权,也称为读所有权,或读锁
- 独占所有权,也称为写所有权,或写锁
独占所有权就像普通互斥对象的所有权一样: 只有一个线程可以持有互斥对象的独占锁,只有这个线程可以释放锁。没有其他线程可以持有互斥锁上的任何类型的锁,而该线程持有其锁。
共享所有权则更为宽松。任意数量的线程可以同时共享互斥对象的所有权。任何线程都不能获得互斥锁,而任何线程都持有共享锁。
这些互斥锁通常用于保护很少更新的共享数据,但如果有线程正在读取这些数据,则无法安全地更新。因此,当读取线程读取数据时,它们共享所有权。当需要修改数据时,修改线程首先获得互斥锁的独占所有权,从而确保没有其他线程在读取它,然后在修改完成后释放独占锁。
Spinlocks(自旋锁)
自旋锁是一种特殊类型的互斥锁,当锁操作必须等待时,它不使用 OS 同步函数。相反,它只是不断尝试更新互斥数据结构,以便在循环中获得锁。
如果不经常持有锁,并且/或只持有很短的时间,那么这比调用重量级线程同步函数更有效。然而,如果处理器不得不循环太多次,那么它只是在浪费时间,什么也不做,如果操作系统安排另一个线程有活动工作要做,而不是线程无法获得自旋锁,那么系统会做得更好。
Semaphores(信号量)
信号量是一种非常轻松的可锁定对象。给定的信号量具有预定义的最大计数和当前计数。使用 wait 操作获得信号量的所有权,也称为递减信号量,或者甚至只是抽象地称为 p。您可以使用信号操作(也称为递增信号量)、 post 操作或抽象的 v 来释放所有权。单字母操作名称来自 Dijkstra 关于信号量的原始论文。
每次等待信号量时,都会减少当前计数。如果计数大于零,那么递减就会发生,等待调用返回。如果计数已经为零,那么它不能递减,因此等待调用将阻塞,直到另一个线程通过信号量信号来增加计数。
每次发出信号信号量时,都会增加当前计数。如果在您调用 signal 之前计数为零,并且有一个线程在等待中被阻塞,那么该线程将被唤醒。如果有多个线程在等待,则只有一个线程会被唤醒。如果计数已经达到其最大值,那么信号通常会被忽略,尽管一些信号量可能会报告错误。
互斥对象的所有权与线程绑定得非常紧密,只有获得互斥对象锁的线程才能释放它,而信号量的所有权则轻松得多,而且时间短暂。任何线程都可以在任何时候向信号量发出信号,而不管该线程是否以前已经等待该信号量。
An analogy(类比)
信号量就像一个没有滞纳金的公共借阅图书馆。他们可能有5个 c + + 并发操作的副本可以借用。前五个来图书馆的人会各得到一份副本,但是第六个人要么不得不等待,要么离开然后再回来。
图书馆不在乎谁还书,因为没有滞纳金,但是当图书馆得到一个复本的时候,它会给一个等待它的人。如果没有人在等,这本书就会一直放在书架上,直到有人想要一本为止。