如果锁对象是锁中的最后一条语句,那么覆盖它是不是不好?
Posted
技术标签:
【中文标题】如果锁对象是锁中的最后一条语句,那么覆盖它是不是不好?【英文标题】:Is it bad to overwrite a lock object if it is the last statement in the lock?如果锁对象是锁中的最后一条语句,那么覆盖它是否不好? 【发布时间】:2018-04-01 21:13:01 【问题描述】:我已经看过几次了,但我不确定它是否真的不正确。
考虑以下示例类:
class Foo
List<string> lockedList = new List<string>();
public void ReplaceList(IEnumerable<string> items)
var newList = new List<string>(items);
lock (lockedList)
lockedList = newList;
public void Add(string newItem)
lock (lockedList)
lockedList.Add(newItem);
public void Contains(string item)
lock (lockedList)
lockedList.Contains(item);
ReplaceList 覆盖lockedList
,而它有一个锁。一旦它完成,所有后续调用者实际上将锁定新值。他们可以在 ReplaceList 退出它的锁之前进入他们的锁。
尽管通过替换锁定对象来引发标志,但此代码实际上可能正常工作。只要分配是锁的最后一条语句,就没有更多同步代码要运行。
除了确保分配保持在锁定块末尾会增加维护成本之外,还有其他原因可以避免这种情况吗?
【问题讨论】:
我想不出它会失败的任何原因,但它仍然让我感到不舒服。 取决于“正确”的含义。在锁定块中,您可以锁定lockedList
最初 指向的对象。您没有将锁定转移到newList
,您永远不会锁定该对象。因此,只要您的代码不假设其他情况,就可以了。
这是否正确将根据未显示的信息而大不相同。这将取决于跨线程共享的所有内容,以及所有各种代码如何使用这些共享对象。这可能没问题,或者不可能,没有办法知道。
C# 编译器对此发出警告,CS0728,请务必尝试一下。提醒你,你做错了。但是确实指出编译器足够聪明,不会让它出错,它会将lockedList复制到一个不可见的变量中。
虽然不是同一个问题,但 Eric Lippert 几乎在同一时间回答了一个类似的问题,并继续解释为什么这种结构是一个坏主意,以及使用可变变量的锁的正确方法是什么是:***.com/a/46854307/517852
【参考方案1】:
因此,首先,由于您访问该字段的具体方式,您提供的特定解决方案并不安全。
获得一个可行的解决方案很容易。只是不要创建新列表;相反,清除它并添加新项目:
class Foo
private List<string> lockedList = new List<string>();
public void ReplaceList(IEnumerable<string> items)
lock (lockedList)
lockedList.Clear();
lockedList.AddRange(items);
public void Add(string newItem)
lock (lockedList)
lockedList.Add(newItem);
public void Contains(string item)
lock (lockedList)
lockedList.Contains(item);
现在您的领域实际上并没有发生变化,您无需担心可能导致的所有问题。
至于问题中的代码如何破解,只需要调用Add
或Contains
来读取字段,获取列表,锁定列表,然后让另一个线程替换该字段。当您第二次读取该字段时,在已经获得要锁定的值之后,该值可能已经更改,因此您最终会改变或从另一个调用者不会被限制访问的列表中读取。
综上所述,虽然改变 lockedList
变量是一个真的坏主意,你应该毫无疑问地避免改变它,如上所示,你还可以确保你只实际读取该字段一次,而不是重复读取它,您仍然可以确保每个列表在任何时候都只能从单个线程访问:
class Foo
private volatile List<string> lockedList = new List<string>();
public void ReplaceList(IEnumerable<string> items)
lockedList = new List<string>(items);
public void Add(string newItem)
var localList = lockedList;
lock (localList)
localList.Add(newItem);
public void Contains(string item)
var localList = lockedList;
lock (localList)
localList.Contains(item);
请注意,此修复的问题不是改变从中获取要锁定的对象的字段(这不是固有的问题,尽管是一种非常糟糕的做法),而是从lock
语句内部不断地从字段在它的所有用法中获取新值,并期望该值在可能的情况下永远不会改变。
这将更难维护,非常脆弱,并且更难以理解或确保虽然的正确性,所以再次做这样的事情。
【讨论】:
哈哈,我只是在寻找“变异锁对象不好”之外的原因,而不是这样做。我从没想过会看到有人“修复”它,所以你可以做这个愚蠢的事情:P 还有另一个真正的修复;以防清除和填充会使锁定时间过长。使用单独的只读锁定对象,以便您可以安全地替换列表。【参考方案2】:我意识到读取锁对象的字段是在我们锁定它之前完成的;所以这永远不会是正确的。如果另一个线程在值更改之前尝试进入锁,它将最终进入其锁定块并锁定旧值,但该字段将具有新值。然后它将使用新值而不对其进行锁定。
例子:
-
线程A调用
ReplaceList
进入锁。
线程 B 调用 Add
。它到达锁并被阻止。
线程 A 将 lockedList
替换为新值。
线程C调用Contains
,得到lockedList
的新值并取出锁。
线程 A 退出其锁定,允许线程 B 恢复。
线程 B 使用 lockedList
的旧值进入锁,并将项目添加到新列表中,而新列表上没有锁定。
线程 C 引发异常,因为列表在枚举时被修改。
【讨论】:
@Servy 我不知道你在想什么样的其他情况。我我假设对该字段的所有访问都被锁定。是的,需要第三次调用才能真正破坏事物。我将清理示例并将其添加到问题中。 请注意,对于第 7 点,问题在于 您不知道它会做什么。有很多种方法可能会破坏。它可能会抛出异常,并且可能会抛出很多异常,它可能会返回错误的结果,它可能会使整个过程崩溃,它可能工作得非常好,或任何其他可能性。它根本没有定义会发生什么,这就是让这样的错误如此难以处理的原因。以上是关于如果锁对象是锁中的最后一条语句,那么覆盖它是不是不好?的主要内容,如果未能解决你的问题,请参考以下文章