概要:
在我看来,这:
将表示逻辑状态的字段包装到单个不可变的消耗对象中
通过调用Interlocked.CompareExchange<T>
更新对象的权威引用
并适当地处理更新失败
提供了一种并发性,不仅使“锁”结构变得不必要,而且使人误以为真正的误导性结构避免了有关并发的某些现实情况,并因此引入了许多新问题。
问题讨论:
首先,让我们考虑使用锁的主要问题:
锁会导致性能下降到,必须同时使用以进行读写。
锁定块线程的执行,阻止并发并有可能使死锁。
考虑一下受“锁”启发的荒谬行为。当需要同时更新逻辑上的资源集时,我们会“锁定”资源集,并通过一个松散关联但专用的锁定对象来进行锁定,否则将无济于事(红色标志#1)。
然后,我们使用“锁定”模式来标记代码区域,在该区域中发生一组数据字段在逻辑上一致的状态变化,但我们却通过将字段与同一对象中无关字段混合在一起而使自己陷入困境,同时让它们全部可变,然后将自己逼入一个角落(红色标记2),在读取这些各个字段时还必须使用锁,因此我们不会陷入不一致的状态。
显然,该设计存在严重问题。这有点不稳定,因为它需要对锁对象进行仔细的管理(锁定顺序,嵌套锁,线程之间的协调,阻塞/等待另一个正在等待您执行操作的线程正在使用的资源等),具体取决于上下文。我们还听到人们谈论避免死锁的方法是“硬性”的,而实际上这非常简单:不要偷偷计划要参加比赛的人的鞋子!
解:
完全停止使用“锁定”。 将字段正确地滚动到一个代表一致状态或架构的不易损坏/不变的对象中。也许它只是一对用于在显示名称和内部标识符之间来回转换的字典,或者它可能是包含值和到下一个对象的链接的队列的头节点;不管是什么,将其包装到自己的对象中并密封以保持一致性。
将写入或更新失败识别为一种可能性,在发生失败时对其进行检测,并根据上下文做出决定,立即(或稍后)重试或执行其他操作,而不是无限期地阻止。
尽管阻塞似乎是对似乎必须完成的任务进行排队的一种简单方法,但并非所有线程都如此专心和自我服务,以至于他们有能力做这样的事情而冒着损害整个系统的风险。用“锁”序列化事物不仅很懒,而且由于试图假装写操作不会失败而带来的副作用是,您阻塞/冻结了线程,因此该线程将响应设置为无响应且无用,从而放弃了其他所有职责。它很顽固地等待着提前完成计划的工作,却忽略了有时需要他人协助来履行自己的职责这一事实。
当同时发生独立的自发动作时,竞争条件是正常的,但是与不受控制的以太网冲突不同,作为程序员,我们可以完全控制我们的“系统”(即确定性数字硬件)及其输入(无论随机性如何,以及随机性如何)。零或一个真的是?)和输出,以及存储我们系统状态的内存,所以活锁应该不是问题;此外,我们拥有带有内存屏障的原子操作,可解决以下事实:可能有许多处理器同时运行。
总结一下:
抓取当前状态对象,使用其数据,并构造一个新状态。
意识到其他 Activity 线程将做同样的事情,并且可能会击败您,但是所有人都观察到代表“当前”状态的权威参考点。
使用Interlocked.CompareExchange同时查看您基于其工作的状态对象是否仍是最新状态,并用新状态替换它,否则失败(因为另一个线程先完成)并采取适当的纠正措施。
最重要的部分是如何处理故障并重新站起来。在这里,我们避免了活锁,思考过多,做得不够或做正确的事情。我想说的是,锁造成了一种幻象,即使骑着踩踏事件,您也永远不会掉下马来,而在这样一个幻想的土地上做白日梦时,系统的其余部分可能会崩溃而崩溃并烧毁。
那么,使用CompareExchange和不可变逻辑状态对象的无锁实现是否有“锁定”构造可以做的事情(更好,以一种不太不稳定的方式)呢?
所有这些都是我在认真处理锁之后才逐渐意识到的,但是在进行一些搜索之后,在另一个线程
Is lock free multithreaded programming making anything easier?中,有人提到当我们面对高度并行的系统时,无锁编程将非常重要。如果我们负担不起使用高度竞争的锁,那么将有数百个处理器。
您的比较交换建议有一个很大的缺点-这不公平,因为它偏爱短时间的工作。如果系统中有许多短任务,则完成长任务的机会可能会非常低。
我是一名优秀的程序员,十分优秀!