当我按下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);
});
}
}
我该如何解决这个问题?或者这是一个不好的做法,让异步和同步调用一个方法?如果是这样,有没有办法限制具有锁的方法只能异步使用?
答案 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在他的回答中所说的那样。