C#死锁调用锁定的方法

本文关键字:方法 锁定 调用 死锁 | 更新日期: 2023-09-27 17:59:23

当我点击rbtn1然后点击rbtn2时,以下代码会导致死锁。Rbtn2是异步的,当我只点击多次rbtn1时,它是可以的。Rbtn2同步的,当我们只点击多次Rbtn2时,它也是可以的。但当我混合它们时,就会发生死锁。为什么会这样?

    private void rbtn1_Click(object sender, EventArgs e)
    {
        Task.Run(() => UpdateDisplayLock("a"));
    }
    private void radButton2_Click(object sender, EventArgs e)
    {
        UpdateDisplayLock("a");
    }
    private object _lockKey = new object();
    private void UpdateDisplayLock(string i)
    {
        lock (_lockKey)
        {
            Interlocked.Increment(ref _uniqueId);
            var uniqueId = _uniqueId;
            Invoke((Action)delegate
            {
                rlblDisplay.Text += Environment.NewLine + uniqueId + string.Format(":{0} Start;", i);
            });
            Thread.Sleep(5000);
            Invoke((Action)delegate
            {
                rlblDisplay.Text += Environment.NewLine + uniqueId + string.Format(":{0} End;", i);
            });
        }
    }

如何解决此问题?或者这只是异步和同步调用方法的一种糟糕做法?如果是这样的话,有没有办法限制带锁的方法只能异步使用?

C#死锁调用锁定的方法

UpdateDisplayLock中,您正在调用UI线程。因此,当你在按下第一个按钮后从另一个线程调用这个方法时,它需要定期访问UI线程才能继续

当你点击第二个按钮时调用UpdateDisplayLock时,它会点击lock,由于后台进程正在按住它,它只会坐在那里等待。您现在正在阻塞UI线程,直到第一个进程完成(这样它就可以释放锁)。

当运行UpdateDisplayLock的后台线程去调用UI线程中的操作时,它会坐在那里等待在UI线程中调度工作。第二个按钮点击就在那里等着你,阻塞了UI线程。

现在有两个线程,每个线程都在等待另一个线程。僵局


至于如何解决这个问题,最好的解决方案是使UpdateDisplayLock成为一个固有的异步操作,而不是一个你可能从另一个线程调用也可能不调用的固有同步操作:

private async Task UpdateDisplayLock(string i)
{
    _uniqueId++;
    var uniqueId = _uniqueId;
    rlblDisplay.Text += Environment.NewLine + 
        uniqueId + string.Format(":{0} Start;", i);
    await Task.Delay(TimeSpan.FromSeconds(5));
    rlblDisplay.Text += Environment.NewLine + 
        uniqueId + string.Format(":{0} End;", i);
}

请注意,在这个实现中,它将允许多个调用交织它们的开始和结束调用,但由于增量和UI操作都在UI线程中,因此不会出现任何线程错误。如果您不希望任何后续调用在前一个调用完成之前都能开始它们的操作/日志记录,那么您可以使用SemaphoreSlim异步执行:

private SemaphoreSlim semaphore = new SemaphoreSlim(1);
private async Task UpdateDisplayLock(string i)
{
    await semaphore.WaitAsync();
    try
    {
        _uniqueId++;
        var uniqueId = _uniqueId;
        rlblDisplay.Text += Environment.NewLine +
            uniqueId + string.Format(":{0} Start;", i);
        await Task.Delay(TimeSpan.FromSeconds(5));
        rlblDisplay.Text += Environment.NewLine +
            uniqueId + string.Format(":{0} End;", i);
    }
    finally
    {
        semaphore.Release();
    }
}

然后,如果你有事情要做,你可以await这个异步方法形成你的事件处理程序,或者如果你在它完成后不需要做任何事情,你可以直接调用它:

private async void rbtn1_Click(object sender, EventArgs e)
{
    await UpdateDisplayLock("a");
    DoSomethingElse();
}
private void radButton2_Click(object sender, EventArgs e)
{
    var updateTask = UpdateDisplayLock("a");
}

如果您按下按钮二,线程进入睡眠状态并锁定,如果您现在按下按钮一,它进入锁定状态,保持GUI线程锁定(GUI现在已经死了),现在第一个线程(具有锁定)完成启动Invoke的睡眠语句。Invoke现在等待GUI线程运行该操作。如果出现死锁,Invoke会阻塞当前线程,直到GUI线程能够处理您的请求。这将永远不会发生,因为您锁定了GUI线程。

绕过死锁的最简单方法是使用BeginInvoke,它在Action完成之前不会阻塞当前线程。但是仍然会导致奇怪的行为,因为GUI更新可能会在操作运行时延迟,从而导致意外行为。

真正的问题在于您在GUI线程中运行了一个5秒的操作。这导致了糟糕的用户体验。以及将线程与invoke混合时的问题。更好地将UpdateDisplayLock实现为Async,并使用SemaphoreSlim来同步多线程,正如Servy在他的回答中发布的那样。