C#死锁调用锁定方法

时间:2015-06-24 16:45:32

标签: c# winforms asynchronous deadlock

当我按下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);
            });
        }
    }

我该如何解决这个问题?或者这是一个不好的做法,让异步和同步调用一个方法?如果是这样,有没有办法限制具有锁的方法只能异步使用?

2 个答案:

答案 0 :(得分:1)

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");
}

答案 1 :(得分:1)

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

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

真正的问题在于您在GUI线程中运行了5秒钟的操作。这会导致糟糕的用户体验。混合线程与调用时的问题。最好将UpdateDisplayLock实现为Async,并使用SemaphoreSlim来同步多线程,就像Servy在他的回答中所说的那样。