如果所有相关线程在使用资源之前都尝试获取它们应该获取的锁,互斥锁是不是才能正常工作?
Posted
技术标签:
【中文标题】如果所有相关线程在使用资源之前都尝试获取它们应该获取的锁,互斥锁是不是才能正常工作?【英文标题】:Do mutexes only function correctly if all relevant threads attempt to acquire the locks they should be acquiring, prior to utilizing a resource?如果所有相关线程在使用资源之前都尝试获取它们应该获取的锁,互斥锁是否才能正常工作? 【发布时间】:2020-10-19 21:34:45 【问题描述】:在第一次参加操作系统课程之前,我只是第一次了解锁。我最初认为锁会从字面上“锁定某些资源”,您需要在其中指定资源(可能通过指向内存中资源地址的指针),但是在阅读了几个非常基本的自旋锁实现之后(例如,类unix训练操作系统“xv6”的版本):
http://pages.cs.wisc.edu/~skobov/cs537/P3/xv6/kernel/spinlock.h http://pages.cs.wisc.edu/~skobov/cs537/P3/xv6/kernel/spinlock.c
以及之前这个堆栈溢出问题:(What part of memory does a mutex lock? (pthreads))
我想我错了。
在我看来,锁实际上只是一个类似于变量的布尔标志,它暂时(或无限期地)阻止执行某些会利用资源的代码,但只有在另一个线程实际上也尝试获取锁的情况下(在第二个线程也尝试获取锁,第二个线程的阻塞具有第二个线程在第一个线程释放锁之前无法利用资源的副作用)。所以现在我想知道:如果一个设计不佳的线程不使用互斥锁并且只是尝试利用另一个设计良好的线程锁定的资源,那么设计不佳的线程是否能够访问该资源(通过简单地忽略互斥锁——我现在认为它是线程应该查看的标志,但有机会忽略)?
如果是这样,那么为什么我们将锁实现为复杂的布尔变量,这样所有线程都必须使用锁,而不是阻止访问内存区域的锁?
由于我对这一切都比较陌生,如果我错误地陈述我的问题以及答案,我感谢任何合理的术语编辑建议!
非常感谢!
--edit,谢谢大家的及时和有用的回复!
【问题讨论】:
您对所有线程应该正确实现对共享资源的访问的理解是正确的。并且不存在“阻止访问内存区域的锁”之类的东西——这需要特殊的硬件支持。 这种现象之前已经观察到了。 Paul McKenney 将 RCU 称为“通过社会工程实现的同步”,并指出锁需要所有调用者都以同样的努力来锁定互斥体。像 Rust 这样的更现代的语言可以为您提供一个 API,您只能通过锁定对象来访问它,而在解锁时您会自动失去访问权限。 Re, "...一些会利用资源的代码..." 另外,不要忘记线程正在“利用”资源,即使它只是 看起来 在内存中的数据。我们使用互斥锁的真正原因是确保没有线程会看到共享变量处于由于其他线程的操作而导致的某些不良/不一致/半途而废的状态。有时,仅仅看到处于不一致状态的变量可能会导致程序线程跟踪错误的指针并导致程序段错误,或者对火箭制导系统发出不适当的“更正”,陷入无限循环等。 【参考方案1】:如果是这样,那么为什么我们将锁实现为复杂的布尔变量,这样所有线程都必须使用锁,而不是使用锁来阻止访问内存区域?
原因很多:
如果您控制访问的对象不是内存区域怎么办?如果是文件或网络连接呢?
编译器如何知道它何时要访问受保护的内存区域?编译器是否必须假设任何地方的任何内存访问都可能与其他线程同步?这将使许多优化变得不可能,包括将可能共享的变量存储在非常关键的寄存器中。
硬件是否必须支持在任何粒度上锁定内存?它如何知道与对象关联的内存是什么?考虑一个链表。您是否必须锁定与该链表和其中的每个对象相关联的每一位内存?当您从列表中添加或删除对象时,您是否必须更改受保护的内存?那岂不是既昂贵又极难使用?
它如何知道何时释放锁?假设您访问了一些需要保护的内存区域,然后您访问了其他一些内存区域。实现如何知道是否允许其他线程访问这两个访问之间的那个区域?实现需要知道访问该区域的代码是否依赖于这两个访问的共享状态的一致视图。它怎么会知道?通过保持锁定和并发受到影响而弄错了。释放两次访问之间的锁会出错,代码的行为可能无法预测。
等等。
【讨论】:
感谢您的帮助!这很有意义!以上是关于如果所有相关线程在使用资源之前都尝试获取它们应该获取的锁,互斥锁是不是才能正常工作?的主要内容,如果未能解决你的问题,请参考以下文章