让我们考虑一下使用没有单独的“可升级读取器”的读取器-写入器锁的不同方式。
根据您的模式,正如您所指出的,步骤 3 和步骤 4 之间存在竞争,其中另一个线程可以获取写入器锁定。更重要的是,在 3 和 4 之间有一个步骤,线程可以获取写入器锁并改变我们在步骤 2 中观察到的状态.
因此,我们有四种选择,具体取决于发生这种情况的可能性:
我们坚持您的方法,因为这实际上是不可能的(例如,给定的状态转换在我们的应用程序中是单向的,因此一旦观察到它就是永久性的)。在这种情况下,我们很可能会进行改造,以便根本不需要锁。 (单向转换适合无锁技术)。
我们首先就获取写入者锁,因为我们在步骤 2 中观察到的状态很可能会发生变化,并且使用读取者锁来检查它是浪费时间。
-
我们将您的步骤更改为:
- 获取读锁
- 检查条件以查看是否需要进行写锁定
- 释放读锁
- 采取写锁定
- 如果情况发生变化,请重新检查。
- 执行更新
- 释放写锁
-
我们改为:
- 在支持递归的锁上获取读锁。
- 检查我们是否需要采取写锁。
- 获取写锁(不释放读)。
- 执行更新。
- 释放写锁。
- 释放读锁。
不难理解为什么 4 对某些人来说更有吸引力,尽管只是稍微难理解它如何使死锁更容易产生。可悲的是,稍微困难一点就足以让很多人只看到优点而看不到缺点。
对于没有发现这一点的人来说,如果两个线程具有读锁,并且其中一个升级为写锁,则它必须等待另一个线程释放读锁。但是,如果第二个线程升级到写锁而不释放读锁,那么它将永远等待第一个线程,而第一个线程将永远等待它。
如上所述,哪种方法最好取决于状态同时发生变化的可能性有多大(或者我想我们想多快地对其做出反应)。即使最后一种非释放升级方法也可以在可行的代码中占有一席之地,只要只能有一个线程尝试在不释放的情况下升级其锁。
除了最后一个选项有效的特殊情况外,其他选项之间的差异都与性能有关,哪个性能最高主要取决于重新检查状态的成本以及由于更改而中止写入的可能性同时。
但是,请注意,所有这些都涉及获取写入器锁,因此它们都具有阻塞所有读取线程的效果,即使写入确实中止也是如此。
可升级读锁为我们提供了一个中间立场,因为虽然它们会阻止写锁和其他可升级读锁,但它们不会阻止读锁。它们可能更好,尽管不是作为可以升级为尚未提交写入的写锁的读锁。*在决定不升级的情况下,对读取线程的影响为零。
这意味着,即使线程决定不更改状态的可能性很小,读取线程也不会受到影响,并且性能改进肯定可以证明其使用的合理性。
*就此而言,“读者-作者”有点用词不当,我们可以例如使用 a 保护整数或对象数组ReaderWriterLockSlim
,使用读锁以原子方式读取和写入单个项目,并使用写锁进行需要读取整个数组而不需要在读取时更改部分数组的操作。在这种情况下,读取操作不需要独占锁,而写入操作则可以使用共享锁。