c# - 任务取消暂停 UI

标签 c# wpf task suspend cancellation

我对以下情况感到有点困惑。如果我调用 SleepBeforeInvoke 方法,应用程序将在 _task.Wait(); 字符串上挂起。但是,如果我调用 SleepAfterInvoke 方法,应用程序工作正常并且控制将到达 catch 子句。调用 BeginInvoke 方法也可以正常工作。

谁能最详细地解释这三种方法用法之间有什么区别?为什么如果我使用 SleepBeforeInvoke 方法,应用程序会被暂停,而如果我使用 SleepAfterInvokeBeginInvoke 方法,应用程序不会被暂停?谢谢。

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 调用,SleepBeforeInvokeSleepAfterInvoke 都有潜在的死锁 - 只是你的情况如此更有可能在 SleepBeforeInvoke 中遇到这个问题,因为您在问题发生的地方人为地创建了 500 毫秒的延迟,而不是在其他情况下的可忽略不计(可能是纳秒)的窗口。

该问题是由于 Dispatcher.InvokeTask.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/

相关文章:

wpf - 如何在 WPF DependencyProperty 检索上放置断点?

c# - 异步 Task.WhenAll 超时

c# - 等待服务任务获取 TaskCanceledException : A task was canceled

c# - 弃用/过时的反转

c# - 当 DataTable 为空时,DataGrid 显示空行

c# - 为什么我不能等待对象类型?

c# - 获取 WPF 控件的当前大小

python - 异步: long running tasks with start delay

c# - 字节数组中的压缩内容以字符串形式解压缩

c# - 如何使用户能够从 ASP.NET MVC 发送电子邮件?