blu*_*utt 18 .net c# multithreading
我正在阅读Albahari关于线程的优秀电子书,并遇到以下情况他提到"一个线程可以以嵌套(可重入)方式重复锁定同一个对象"
lock (locker)
lock (locker)
lock (locker)
{
// Do something...
}
Run Code Online (Sandbox Code Playgroud)
以及
static readonly object _locker = new object();
static void Main()
{
lock (_locker)
{
AnotherMethod();
// We still have the lock - because locks are reentrant.
}
}
static void AnotherMethod()
{
lock (_locker) { Console.WriteLine ("Another method"); }
}
Run Code Online (Sandbox Code Playgroud)
根据解释,任何线程都会阻塞第一个(最外面的)锁,并且只有在外部锁退出后才会解锁.
他说"当一个方法在一个锁中调用另一个方法时,嵌套锁定很有用"
为什么这有用?你什么时候需要这样做,它解决了什么问题?
SLa*_*aks 11
比方说,你有两个公共方法,A()并且B(),它们都需要相同的锁.
而且,让我们说这些A()电话B()
由于客户端也可以B()直接调用,因此需要锁定这两种方法.
因此,当A()被调用时,B()将再次进行锁定.
Jon*_*nna 10
这样做并不是很有用,因为它被允许这样做很有用.考虑如何经常使用调用其他公共方法的公共方法.如果公共方法调用了lock,并且调用它的公共方法需要锁定它所做的更广泛的范围,那么能够使用递归锁意味着你可以这样做.
在某些情况下,您可能会觉得使用两个锁定对象,但是您将一起使用它们,因此如果您犯了错误,则存在很大的死锁风险.如果你可以处理给予锁的更广泛的范围,那么对于这两种情况使用相同的对象 - 并且在你将使用两个对象的情况下递归 - 将删除那些特定的死锁.
然而...
这个用处是值得商榷的.
在第一个案例中,我将引用Joe Duffy的话:
递归通常表示同步设计中的过度简化通常会导致代码不太可靠.一些设计使用锁递归作为一种方法来避免将函数拆分为带锁的函数和假设已经采用锁的函数.这无疑会导致代码大小的减少,从而缩短写入时间,但最终导致设计更加脆弱.将代码分解为采用非递归锁的公共入口点以及断言锁定的内部工作函数始终是一个更好的主意.递归锁定调用是导致原始性能开销的冗余工作.但更糟糕的是,取决于递归会使您更难理解程序的同步行为,特别是在不变量应该保持的边界.通常我们想说锁定获取后的第一行代表对象的一个不变的"安全点",但是一旦引入了递归,就不再能够自信地表达这个语句.这反过来使得在动态组合时更难以确保正确和可靠的行为.
(乔在他的博客中的其他地方以及他关于并发编程的书中有更多的话要说.
第二种情况是通过递归锁定进入只会使不同类型的死锁发生的情况来平衡,或者推高争用率以至于可能存在死锁(这个人说他更喜欢它只是为了解决僵局你第一次递归时,我不同意 - 我更喜欢它只是为了抛出一个很大的异常,它带来了一个很好的堆栈跟踪我的应用程序).
更糟糕的是,它是在错误的时间简化:当你编写代码时,使用锁定递归比分解更多东西更简单,更深入地思考什么时候应该锁定.但是,当您调试代码时,保留锁定这一事实并不意味着使该锁定变得复杂.这是一个多么糟糕的方式 - 当我们认为我们知道我们正在做什么时,复杂的代码是在你的休息时间享受的诱惑,所以你不要在时间上放纵,当我们意识到我们搞砸了我们最希望事情变得美好而简单.
嘿,POSIX线程只有它们才有胆量!
至少lock关键字意味着我们避免了Monitor.Exit()每个Monitor.Enter()s 都没有匹配s 的可能性,这使得一些风险不太可能发生.直到您需要在该模型之外执行某些操作.
使用更新的锁定类,.NET可以帮助人们避免使用锁定递归,而不会阻止那些使用旧编码模式的人.ReaderWriterLockSlim有一个构造函数重载,允许你使用它递归,但默认是LockRecursionPolicy.NoRecursion.
通常在处理并发问题时,我们必须在更加充分的技术之间做出决定,这种技术可能会给我们带来更好的并发性,但需要更加谨慎,以确保正确性与更简单的技术相比,这种技术可能会带来更糟的并发性,但它在哪里更容易确定正确性.以递归方式使用锁为我们提供了一种技术,在这种技术中,我们将保持锁更长并且并发性较差,并且不太确定正确性并且调试更难.