我对以下情况感到有点困惑。如果我调用 SleepBeforeInvoke
方法,应用程序将在 _task.Wait();
字符串上挂起。但是,如果我调用 SleepAfterInvoke
方法,应用程序工作正常并且控制将到达 catch
子句。调用 BeginInvoke
方法也可以正常工作。
谁能最详细地解释这三种方法用法之间有什么区别?为什么如果我使用 SleepBeforeInvoke
方法,应用程序会被暂停,而如果我使用 SleepAfterInvoke
和 BeginInvoke
方法,应用程序不会被暂停?谢谢。
Win 7、.Net 4.0
xaml:
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Name="_textBlock"
Text="MainWindow"></TextBlock>
<Button Grid.Row="1"
Click="ButtonBase_OnClick"></Button>
</Grid>
.cs:
public partial class MainWindow : Window
{
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private Task _task;
/// <summary>
/// Application wiil be suspended on string _task.Wait();
/// </summary>
private void SleepBeforeInvoke()
{
for (Int32 count = 0; count < 50; count++)
{
if (_cts.Token.IsCancellationRequested)
_cts.Token.ThrowIfCancellationRequested();
Thread.Sleep(500);
Application.Current.Dispatcher.Invoke(new Action(() => { }));
}
}
/// <summary>
/// Works fine, control will reach the catch
/// </summary>
private void SleepAfterInvoke()
{
for (Int32 count = 0; count < 50; count++)
{
if (_cts.Token.IsCancellationRequested)
_cts.Token.ThrowIfCancellationRequested();
Application.Current.Dispatcher.Invoke(new Action(() => { }));
Thread.Sleep(500);
}
}
/// <summary>
/// Works fine, control will reach the catch
/// </summary>
private void BeginInvoke()
{
for (Int32 count = 0; count < 50; count++)
{
if (_cts.Token.IsCancellationRequested)
_cts.Token.ThrowIfCancellationRequested();
Thread.Sleep(500);
Application.Current.Dispatcher.BeginInvoke(new Action(() => { }));
}
}
public MainWindow()
{
InitializeComponent();
_task = Task.Factory.StartNew(SleepBeforeInvoke, _cts.Token, TaskCreationOptions.None, TaskScheduler.Default);
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
try
{
_cts.Cancel();
_task.Wait();
}
catch (AggregateException)
{
}
Debug.WriteLine("Task has been cancelled");
}
}
最佳答案
由于 Dispatcher.Invoke
调用,SleepBeforeInvoke
和 SleepAfterInvoke
都有潜在的死锁 - 只是你的情况如此更有可能在 SleepBeforeInvoke
中遇到这个问题,因为您在问题发生的地方人为地创建了 500 毫秒的延迟,而不是在其他情况下的可忽略不计(可能是纳秒)的窗口。
该问题是由于 Dispatcher.Invoke
和 Task.Wait
的阻塞性质造成的。 SleepBeforeInvoke
的流程大致如下:
The app starts and the task is wired up.
The task runs on a thread pool thread, but periodically blocks on a synchronous call marshalled to your UI (dispatcher) synchronization context. The task has to wait for this call to complete before it can proceed to the next loop iteration.
When you press the button, cancellation will be requested. It will most likely happen while the task is executing
Thread.Sleep
. Your UI thread will then block waiting for the task to finish (_task.Wait
), which will never occur, because right after your task finishes sleeping it won't check whether it's been cancelled and will try to make a synchronous dispatcher call (on the UI thread, which is already busy due to_task.Wait
), and ultimately deadlock.
您可以(在某种程度上)通过在 sleep 后再次使用 _cts.Token.ThrowIfCancellationRequested();
来解决此问题。
在 SleepAfterInvoke
示例中没有观察到问题的原因是时间安排:您的 CancellationToken
始终在同步调度程序调用之前检查,因此,在检查和调度程序调用之间发生对 _cts.Cancel
的调用的可能性可以忽略不计,因为两者非常接近。
您的 BeginInvoke
示例根本不存在上述行为,因为您正在删除导致死锁的东西 - 阻塞调用。 Dispatcher.BeginInvoke 是非阻塞的 - 它只是在将来的某个时间“安排”对调度程序的调用,并立即返回而不等待调用完成,从而允许线程池任务继续进行下一个循环迭代,然后点击 ThrowIfCancellationRequested
。
只是为了好玩:我建议您在传递给 Dispatcher.BeginInvoke
的委托(delegate)中放置类似 Debug.Print
的内容,另一个紧接在 _task.Wait
之后。您会注意到,它们不会按照您期望的顺序执行,因为 _task.Wait
会阻塞 UI 线程,这意味着委托(delegate)会在之后传递给 Dispatcher.BeginInvoke
在按钮处理程序完成运行之前,请求的取消不会执行。
关于c# - 任务取消暂停 UI,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25779748/