如果在原始数据访问周围只使用 lock 关键字,是否可以在 C# 中创建死锁
本文关键字:是否 死锁 创建 lock 周围 访问 原始数据 如果 关键字 | 更新日期: 2023-09-27 18:01:00
我写了很多多线程的C#代码,我发布的任何代码都没有死锁。
我使用以下经验法则:
- 我倾向于只使用
lock
关键字(我也使用其他技术,例如读取器/写入器锁定,但要谨慎使用,并且仅在速度需要时才使用(。 - 如果我正在处理
long
,我会使用Interlocked.Increment
.
我倾向于 - 使用最小的粒度锁定单元:我只倾向于锁定原始数据结构,如
long
、dictionary
或list
。
我想知道如果始终遵循这些规则,是否有可能产生死锁,如果是这样,代码会是什么样子?
更新
我还使用以下经验法则:
- 避免在可能无限期暂停的任何内容(尤其是 I/O 操作(周围添加锁定。如果绝对必须这样做,请确保锁中的所有内容都将在设定的
TimeSpan
后超时。 - 我用于锁定的对象始终是专用对象,例如
object _lockDict = new object();
然后lock(_lockDict) { // Access dictionary here }
.
更新
乔恩·斯基特的好答案。这也证实了为什么我从不遇到死锁,因为我倾向于本能地避免嵌套锁,即使我确实使用它们,我也总是本能地保持入场顺序一致。
为了回应我关于倾向于只使用lock
关键字的评论,即使用 Dictionary
+ lock
而不是 ConcurrentDictionary
,Jon Skeet 发表了以下评论:
@Contango:我也正是这样做的。 我会选择简单的代码,每次都锁定"聪明"的无锁代码,直到有证据表明它会导致问题。
是的,很容易死锁,而无需实际访问任何数据:
private readonly object lock1 = new object();
private readonly object lock2 = new object();
public void Method1()
{
lock(lock1)
{
Thread.Sleep(1000);
lock(lock2)
{
}
}
}
public void Method2()
{
lock(lock2)
{
Thread.Sleep(1000);
lock(lock1)
{
}
}
}
几乎同时调用Method1
和Method2
,繁荣 - 僵局。每个线程都将等待"内部"锁,另一个线程已将其作为其"外部"锁获取。
如果你确保你总是以相同的顺序获取锁(例如,"除非你已经拥有lock1
,否则永远不要获取lock2
"并以相反的顺序释放锁(如果你使用lock
获取/释放,这是隐含的(,那么你就不会得到那种死锁。
您仍然可以使用异步代码获得死锁,只涉及单个线程 - 但这也涉及Task
:
public async Task FooAsync()
{
BarAsync().Wait(); // Don't do this!
}
public async Task BarAsync()
{
await Task.Delay(1000);
}
如果从 WinForms 线程运行该代码,则会在单个线程中死锁 - FooAsync
将阻止 BarAsync
返回的任务,并且 BarAsync
的延续将无法运行,因为它正在等待返回到 UI 线程。基本上,您不应该发出来自 UI 线程的阻止调用......
只要你只锁定一件事是不可能的,如果一个线程试图锁定多个锁,那么是的。餐饮哲学家的问题很好地说明了由简单数据引起的简单僵局。
正如其他答案已经显示的那样;
void Thread1Method()
{
lock (lock1)
{
// Do smth
lock (lock2)
{ }
}
}
void Thread2Method()
{
lock (lock2)
{
// Do smth
lock (lock2)
{ }
}
}
Skeet所写内容的附录:
问题通常不在于"只有"两个lock
...(显然即使只有两个lock
也可能有,但我们想在困难模式下玩:-((...
假设在您的程序中有 10 个lock
资源......让我们称它们为a1
...a10
.您必须确保始终以相同的顺序lock
它们,即使对于它们的子集也是如此......如果一个方法需要a3
、a5
和a7
,而另一个方法需要a4
、a5
、a7
,你必须确保两者都会尝试以"正确"的顺序lock
它们。为简单起见,在这种情况下,顺序很明确: a1
-> a10
.
通常lock
对象没有编号,和/或它们不是在单一方法中获取的......例如:
void MethodA()
{
lock (Lock1)
{
CommonMethod();
}
}
void MethodB()
{
lock (Lock3)
{
CommonMethod();
}
}
void CommonMethod()
{
lock (Lock2)
{
}
}
void MethodC()
{
lock (Lock1)
{
lock (Lock2)
{
lock (Lock3)
{
}
}
}
}
在这里,即使Lock*
编号,也不清楚lock
s 是否可以以错误的顺序取(MethodB
+ CommonMethod
取Lock3
+ Lock2
,而MethodC
取Lock1
+ Lock2
+ Lock3
(...目前还不清楚,我们正在玩三个非常大的优势:我们说的是死锁,所以我们正在寻找它们,lock
是编号的,整个代码大约 30 行。