C# 死锁调用锁定的方法
C# Deadlock Calling Locked Methods
当我依次点击 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);
});
}
}
我该如何解决这个问题?还是异步和同步调用方法只是一种不好的做法?如果是这样,有没有办法限制带锁的方法只能异步使用?
在 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 线程中,因此不会是任何线程错误。如果您不希望任何后续调用能够开始它们的 operation/logging 直到前一个调用完成,那么您可以使用 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 现在会等到 GUI 线程 运行 执行操作。此处发生死锁,Invoke 会阻塞当前线程,直到 GUI 线程能够处理您的请求。这永远不会发生,因为您锁定了 GUI 线程。
解决死锁的最简单方法是使用 BeginInvoke,它在 Action 完成之前不会阻塞当前线程。但仍会导致奇怪的行为,因为您的 GUI 更新可能会延迟到操作 运行 秒,从而导致意外行为。
真正的问题在于您 运行 在 GUI 线程中进行了 5 秒的操作。这会导致糟糕的用户体验。以及将线程与调用混合时的问题。更好地将 UpdateDisplayLock 实现为 Async 并使用 SemaphoreSlim 同步多线程,如 Servy 在他的答案中发布的那样。
当我依次点击 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);
});
}
}
我该如何解决这个问题?还是异步和同步调用方法只是一种不好的做法?如果是这样,有没有办法限制带锁的方法只能异步使用?
在 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 线程中,因此不会是任何线程错误。如果您不希望任何后续调用能够开始它们的 operation/logging 直到前一个调用完成,那么您可以使用 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 现在会等到 GUI 线程 运行 执行操作。此处发生死锁,Invoke 会阻塞当前线程,直到 GUI 线程能够处理您的请求。这永远不会发生,因为您锁定了 GUI 线程。
解决死锁的最简单方法是使用 BeginInvoke,它在 Action 完成之前不会阻塞当前线程。但仍会导致奇怪的行为,因为您的 GUI 更新可能会延迟到操作 运行 秒,从而导致意外行为。
真正的问题在于您 运行 在 GUI 线程中进行了 5 秒的操作。这会导致糟糕的用户体验。以及将线程与调用混合时的问题。更好地将 UpdateDisplayLock 实现为 Async 并使用 SemaphoreSlim 同步多线程,如 Servy 在他的答案中发布的那样。