内核锁

从介绍并发性,我们看到在并发编程的一个基本问题:我们想要自动执行一系列指令,但由于中断的存在在一个处理器(或多个线程上执行多个处理器同时),我们就t。在这一章,我们因此直接攻击这一问题,通过引入一些称为锁。 程序员用锁注释源代码,把它们放在关键的部分,从而确保任何这样的关键部分执行起来就像它是一个单一的原子指令一样。
28.1锁:基本思想。
作为一个例子,假设我们的关键部分是这样的,一个共享变量的规范更新。
当然,其他关键部分是可能的,例如将一个元素添加到一个链接列表或其他更复杂的共享结构更新, 我们先来看看这个简单的例子。要使用锁,我们添加。 关键部分的代码如下:

锁只是一个变量,因此要使用一个锁,必须声明某种类型的锁变量(如上面的互斥锁)。这个锁变量(或者简称为lock)可以在任何时刻保持锁的状态。它可以是可用的(也可以是解锁的或免费的),因此没有线程持有锁,或者获得(或锁定或持有),因此只有一个线程持有锁,并且假定是在一个关键的部分。
我们还可以将其他信息存储在数据类型中,比如哪个线程持有锁,或者一个队列用于订购锁捕获,但是这样的信息隐藏在锁的用户中。lock()和unlock()例程的语义很简单。调用常规锁()试图获取锁;如果没有其他线程持有锁(即:,它是自由的),线程将获得锁并进入关键部分;这个线程有时被认为是锁的所有者。
如果另一个线程在同一个锁变量(本例中是互斥锁)上调用lock(),那么当锁被另一个线程持有时,它将不会返回;这样,其他线程就不能进入关键部分,而持有锁的第一个线程在那里。

旦锁的拥有者调用unlock(),锁就会再次获得(免费)。如果没有其他线程在等待锁(即:,没有其他线程调用lock()并被卡在其中),锁的状态被简单地更改为自由。如果有等待的线程(卡在lock()中),其中一个将(最终)通知(或被告知)这个锁的状态的更改,获取锁,并进入关键部分锁为编程人员提供了一些最低限度的控制。一般来说,我们将线程视为由程序员创建的实体,但按照操作系统所选择的任何方式由操作系统安排。锁定将部分控制权返还给程序员;通过对代码段进行锁定,程序员可以保证在该代码中不可能有任何一个线程处于活动状态。因此,锁定有助于将传统操作系统调度的混乱转化为更可控的活动。
28.2 Pthread锁
POSIX库用于锁的名称是一个互斥锁,因为它用于在线程之间提供互斥,即:如果一个线程在关键部分,则它排除其他线程进入,直到完成该部分为止。因此,当您看到以下POSIX线程代码时,您应该理解它正在做与上面相同的事情(我们再次使用我们的包装器来检查锁定和解锁的错误)。
您可能还注意到POSIX版本通过一个变量来锁定和解锁,因为我们可能使用不同的锁来保护不同的变量。这样做可以提高并发性:而不是一个大锁使用的任何时间任何临界区访问(粗粒度锁策略),一个经常会保护不同的数据和数据结构与不同的锁,从而允许多个线程同时出现在锁定的代码(更细粒度的方法)。
28.3建立一个锁
现在,您应该从程序员的角度了解锁的工作原理。但是我们应该如何构建一个锁呢?需要什么硬件支持?操作系统支持什么?这是我们在本章余下部分要讨论的一系列问题。 要构建一个工作锁,我们需要来自我们的老朋友,硬件,以及我们的好朋友OS的一些帮助多年来,许多不同的硬件原语被添加到各种计算机体系结构的指令集;虽然我们不会研究这些指令是如何实现的(毕竟,这是计算机体系结构类的主题),我们将研究如何使用它们,以便构建一个像锁一样的互斥的原语。 我们还将研究操作系统如何参与完成这幅图,并使我们能够构建一个复杂的锁定库。
28.4评估锁
在构建任何锁之前,我们应该首先了解我们的目标是什么,因此我们会问如何评估特定锁实现的有效性。为了评估锁是否有效(以及是否有效),我们首先应该建立一些基本的标准。首先是锁是否完成了它的基本任务,即提供互斥。基本上,锁工作,防止多个线程进入一个临界区。
第二个是公平。每一个线程都争着在获得它的自由之后获得一个公平的机会吗?另一种看待这个问题的方法是考察更极端的情况:在这样做的过程中,是否有任何线程与锁相争,从而永远得不到它。
最终的标准是性能,特别是使用锁添加的时间开销。这里有几个不同的案例值得考虑。一个是没有争论的情况;当一个线程正在运行并获取并释放锁时,这样做的开销是什么?另一种情况是多个线程争用一个CPU上的锁;在这种情况下,是否存在性能问题?最后,当涉及多个cpu时,锁是如何执行的,并且每个线程都在为锁进行争用?通过比较这些不同的场景,我们可以更好地理解。
28.5控制中断
用于提供互斥的最早解决方案之一是禁用临界段的中断;这个解决方案是为单处理器系统而发明的。代码是这样的。

假设我们在这样一个单处理器系统上运行。在进入临界区之前,通过关闭中断(使用某种特殊的硬件指令),我们可以确保临界段内的代码不会被中断,从而像原子一样执行。当我们完成时,我们重新启用中断(再次通过硬件指令),因此程序照常进行
这种方法的主要优点是简单。你当然不需要绞尽脑汁才能明白为什么会这样。没有中断,线程可以确定它执行的代码将执行,并且没有其他线程会干扰它。 不幸的是,负面因素很多。首先,这种方法要求我们允许任何调用线程执行特权操作(关闭和关闭中断),从而信任这个工具不会被滥用。正如您已经知道的,任何时候我们需要相信一个任意的程序,我们可能会遇到麻烦。在这里,问题表现在许多方面:一个贪婪的程序可以在执行开始时调用lock(),从而独占处理器;更糟糕的是,一个错误或恶意程序可以调用lock()并进入一个循环。在后一种情况下,操作系统永远不会恢复。
第二,这种方法不适用于多处理器。如果多个线程在不同的cpu上运行,并且每个线程都试图进入相同的临界区,那么中断是否被禁用并不重要;线程将能够在其他处理器上运行,从而可以进入关键部分。由于多处理器现在已经司空见惯,我们的通用解决方案将不得不做得更好。
第三,长时间关闭中断会导致中断丢失,从而导致严重的系统问题。例如,假设CPU错过了一个磁盘设备完成了读取请求的事实。操作系统如何知道唤醒等待读的进程?
最后,而且可能最不重要的是,这种方法可能是低效的。与正常的指令执行相比,掩码或非掩码中断的代码往往会被现代cpu缓慢地执行
出于这些原因,关闭中断只在有限的环境中使用,作为互斥的原语。
例如,在某些情况下,操作系统本身会使用中断屏蔽来保证在访问自己的数据结构时原子性,或者至少防止某些混乱的中断处理情况出现。这种用法是有意义的,因为信任问题在操作系统内部消失了,而操作系统始终相信自己能够执行特权操作。
28.6失败的尝试:只使用负载/存储
为了超越基于中断的技术,我们将不得不依赖CPU硬件和它提供给我们的指令来构建一个适当的锁。让我们首先使用一个标记变量来构建一个简单的锁。在这次失败的尝试中,我们将看到构建一个锁所需的一些基本思想,并且(希望)明白为什么仅仅使用一个变量并通过正常的负载和存储来访问它是不够的。在第一次尝试中(图28.1),这个想法非常简单:使用一个简单的变量(标志)来指示某个线程是否拥有一个锁。进入关键部分的第一个线程将调用lock(),它将测试标志是否等于1(在本例中,它不是),然后将标志设置为1,表示线程现在持有锁。当完成关键部分时,线程调用unlock()并清除标记,从而指示锁不再被持有
如果另一个线程碰巧在第一个线程所在的时候调用lock()。 关键部分,它只会在while循环中进行自旋。 线程调用unlock()并清除标记。一旦第一个线程完成。 因此,等待的线程将从while循环中退出,将标志设置为1。 然后进入关键部分。
不幸的是,代码有两个问题:一个是正确性,另一个是性能。一旦您习惯了对并发编程的思考,正确性问题就很简单了。想象图28.2(第6页)中的代码交错;假设国旗= 0开始。
正如您从这个交叉中看到的,及时(不及时?)中断,我们可以很容易地产生一个例子,两个线程都将标志设置为1,并且两个线程都能够进入临界区。这种行为是专业人士所称的“坏”——我们显然未能提供最基本的条件:相互排斥。
我们稍后将讨论的性能问题是,线程等待获取已持有的锁的方式:它无休止地检查标记的值,这是一种称为自旋等待的技术。自旋等待浪费时间等待另一个线程释放锁。在一个单处理器上,浪费是非常高的,在那里,服务员正在等待的线程甚至不能运行(至少,在发生上下文切换之前)!因此,当我们前进和发展更复杂的解决方案时,我们也应该考虑避免这种浪费的方法。
28.7使用测试集构建工作旋转锁
因为禁用中断不能在多个处理器上工作,而且由于使用负载和存储的简单方法(如上所示)不工作,系统设计人员开始为锁定发明硬件支持。最早的多处理器系统,如20世纪60年代早期的Burroughs B5000 [M82],得到了这样的支持;现在所有的系统都提供这种类型的支持,即使对于单CPU系统也是如此要理解的最简单的硬件支持就是所谓的测试-设置指令,也称为原子交换。我们通过下面的C代码片段来定义测试和设置的指令。
测试和设置指令的作用如下。它返回由ptr指向的旧值,并同时更新对new的值。当然,关键是这个操作序列是原子执行的

它之所以被称为test和set,是因为它使您能够“测试”旧值(即返回的值),同时将内存位置“设置”为一个新值;事实证明,这一点。

更强大的指令足以构建一个简单的自旋锁,如图28.3所示。 或者更好的是:先自己想清楚!。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值