如果在原始数据访问周围只使用 lock 关键字,是否可以在 C# 中创建死锁

本文关键字:是否 死锁 创建 lock 周围 访问 原始数据 如果 关键字 | 更新日期: 2023-09-27 18:01:00

我写了很多多线程的C#代码,我发布的任何代码都没有死锁。

我使用以下经验法则:

  1. 我倾向于只使用 lock 关键字(我也使用其他技术,例如读取器/写入器锁定,但要谨慎使用,并且仅在速度需要时才使用(。
  2. 如果我正在处理long,我会使用Interlocked.Increment.
  3. 我倾向于
  4. 使用最小的粒度锁定单元:我只倾向于锁定原始数据结构,如longdictionarylist

我想知道如果始终遵循这些规则,是否有可能产生死锁,如果是这样,代码会是什么样子?

更新

我还使用以下经验法则:

  1. 避免在可能无限期暂停的任何内容(尤其是 I/O 操作(周围添加锁定。如果绝对必须这样做,请确保锁中的所有内容都将在设定的TimeSpan后超时。
  2. 我用于锁定的对象始终是专用对象,例如 object _lockDict = new object();然后lock(_lockDict) { // Access dictionary here }.

更新

乔恩·斯基特的好答案。这也证实了为什么我从不遇到死锁,因为我倾向于本能地避免嵌套锁,即使我确实使用它们,我也总是本能地保持入场顺序一致。

为了回应我关于倾向于只使用lock关键字的评论,即使用 Dictionary + lock 而不是 ConcurrentDictionary ,Jon Skeet 发表了以下评论:

@Contango:我也正是这样做的。 我会选择简单的代码,每次都锁定"聪明"的无锁代码,直到有证据表明它会导致问题。

如果在原始数据访问周围只使用 lock 关键字,是否可以在 C# 中创建死锁

是的,很容易死锁,而无需实际访问任何数据:

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)
        {
        }
    }
}

几乎同时调用Method1Method2,繁荣 - 僵局。每个线程都将等待"内部"锁,另一个线程已将其作为其"外部"锁获取。

如果你确保你总是以相同的顺序获取锁(例如,"除非你已经拥有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它们,即使对于它们的子集也是如此......如果一个方法需要a3a5a7,而另一个方法需要a4a5a7,你必须确保两者都会尝试以"正确"的顺序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 + CommonMethodLock3 + Lock2 ,而MethodCLock1 + Lock2 + Lock3 (...目前还不清楚,我们正在玩三个非常大的优势:我们说的是死锁,所以我们正在寻找它们,lock是编号的,整个代码大约 30 行。