> For the complete documentation index, see [llms.txt](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec10-multiprocessors-and-locking/10.3-when-use-lock.md).

# 10.3 什么时候使用锁？

很明显，锁限制了并发性，也限制了性能。那这带来了一个问题，什么时候才必须要加锁呢？我这里会给你们一个非常保守同时也是非常简单的规则：如果两个进程访问了一个共享的数据结构，并且其中一个进程会更新共享的数据结构，那么就需要对于这个共享的数据结构加锁。

![](/files/-MPHFsAyq6JFa34zETh1)

这是个保守的规则，如果一个数据结构可以被多个进程访问，其中一个进程会更新这个数据，那么可能会产生race condition，应该使用锁来确保race condition不会发生。

但是同时，这条规则某种程度上来说又太过严格了。如果有两个进程共享一个数据结构，并且其中一个进程会更新这个数据结构，在某些场合不加锁也可以正常工作。不加锁的程序通常称为lock-free program，不加锁的目的是为了获得更好的性能和并发度，不过lock-free program比带锁的程序更加复杂一些。这节课的大部分时间我们还是会考虑如何使用锁来控制共享的数据，因为这已经足够复杂了，很多时候就算直接使用锁也不是那么的直观。

矛盾的是，有时候这个规则太过严格，而有时候这个规则又太过宽松了。除了共享的数据，在一些其他场合也需要锁，例如对于printf，如果我们将一个字符串传递给它，XV6会尝试原子性的将整个字符串输出，而不是与其他进程的printf交织输出。尽管这里没有共享的数据结构，但在这里锁仍然很有用处，因为我们想要printf的输出也是序列化的。

所以，这条规则并不完美，但是它已经是一个足够好的指导准则。

> 学生提问：有没有可能两个进程同时acquire锁，然后同时修改数据？
>
> Franz教授：不会的，对于锁来说不可能同时被两个进程acquire，我们之后会看到acquire是如何实现的，现在从acquire的说明来看，任何时间最多只能有一个进程持有锁。

因为有了race condition，所以需要锁。我们之前在kfree函数中构造的race condition是很容易被识别到的，实际上如果你使用race detection工具，就可以立即找到它。但是对于一些更复杂的场景，就不是那么容易探测到race condition。

那么我们能通过自动的创建锁来自动避免race condition吗？如果按照刚刚的简单规则，一旦我们有了一个共享的数据结构，任何操作这个共享数据结构都需要获取锁，那么对于XV6来说，每个结构体都需要自带一个锁，当我们对于结构体做任何操作的时候，会自动获取锁。

可是如果我们这样做的话，结果就太过严格了，所以不能自动加锁。接下来看一个具体的例子。

![](/files/-MPJLWRpCi_6xZDI6_4F)

假设我们有一个对于rename的调用，这个调用会将文件从一个目录移到另一个目录，我们现在将文件d1/x移到文件d2/y。

如果我们按照前面说的，对数据结构自动加锁。现在我们有两个目录对象，一个是d1，另一个是d2，那么我们会先对d1加锁，删除x，之后再释放对于d1的锁；之后我们会对d2加锁，增加y，之后再释放d2的锁。这是我们在使用自动加锁之后的一个假设的场景。

![](/files/-MPMbipXO98admOpvsPt)

在这个例子中，我们会有错误的结果，那么为什么这是一个有问题的场景呢？为什么这个场景不能正常工作？

在我们完成了第一步，也就是删除了d1下的x文件，但是还没有执行第二步，也就是创建d2下的y文件时。其他的进程会看到什么样的结果？是的，其他的进程会看到文件完全不存在。这明显是个错误的结果，因为文件还存在只是被重命名了，文件在任何一个时间点都是应该存在的。但是如果我们按照上面的方式实现锁的话，那么在某个时间点，文件看起来就是不存在的。

所以这里正确的解决方法是，我们在重命名的一开始就对d1和d2加锁，之后删除x再添加y，最后再释放对于d1和d2的锁。&#x20;

![](/files/-MPMd4CdT80r9WukCk5o)

在这个例子中，我们的操作需要涉及到多个锁，但是直接为每个对象自动分配一个锁会带来错误的结果。在这个例子中，锁应该与操作而不是数据关联，所以自动加锁在某些场景下会出问题。

> 学生提问：可不可以在访问某个数据结构的时候，就获取所有相关联的数据结构的锁？
>
> Frans教授：这是一种实现方式。但是这种方式最后会很快演进成big kernel lock，这样你就失去了并发执行的能力，但是你肯定想做得更好。这里就是使用锁的矛盾点了，如果你想要程序简单点，可以通过coarse-grain locking（注，也就是大锁），但是这时你就失去了性能。
