简体   繁体   English

在UI线程上同步取消待处理的任务

[英]Cancelling a pending task synchronously on the UI thread

Sometimes, once I have requested the cancellation of a pending task with CancellationTokenSource.Cancel , I need to make sure the task has properly reached the cancelled state , before I can continue. 有时,一旦我使用CancellationTokenSource.Cancel请求取消了待处理的任务,我需要确保任务已正确达到canceled状态 ,然后才能继续。 Most often I face this situation when the app is terminating and I want to cancel all pending task gracefully. 当应用程序终止时,我通常会遇到这种情况,而我想优雅地取消所有待处理的任务。 However, it can also be a requirement of the UI workflow specification, when the new background process can only start if the current pending one has been fully cancelled or reached its end naturally. 但是,这也可能是UI工作流规范的要求,当新的后台进程仅在当前的未决进程已完全取消或自然结束时才能启动。

I'd appreciate if someone shares his/her approach in dealing with this situation. 如果有人分享他/她的方式来处理这种情况,我将不胜感激。 I'm talking about the following pattern: 我正在谈论以下模式:

_cancellationTokenSource.Cancel();
_task.Wait();

As is, it is known to be capable of easily causing a deadlock when used on the UI thread. 照原样,众所周知,当在UI线程上使用时,能够轻松导致死锁。 However, it is not always possible to use an asynchronous wait instead (ie await task ; eg, here is one of the cases when it is possible). 然而,它并不总是可以使用一个异步等待,而不是(即await task ;例如, 这里是当它可能的情形之一)。 At the same time, it is a code smell to simply request the cancellation and continue without actually observing its state. 同时,简单地请求取消并继续而不实际观察其状态是一种代码味道。

As a simple example illustrating the problem, I may want to make sure the following DoWorkAsync task has been fully cancelled inside FormClosing event handler. 作为说明问题的简单示例,我可能要确保在FormClosing事件处理程序中完全取消了以下DoWorkAsync任务。 If I don't wait for the _task inside MainForm_FormClosing , I may not even see the "Finished work item N" trace for the current work item, as the app terminates in the middle of a pending sub-task (which is executed on a pool thread). 如果我不等待MainForm_FormClosing内的_task ,我可能甚至看不到当前工作项的"Finished work item N"跟踪,因为该应用程序终止于待处理的子任务(在池线程)。 If I do wait though, it results in a deadlock: 如果我确实等待,则会导致死锁:

public partial class MainForm : Form
{
    CancellationTokenSource _cts;
    Task _task;

    // Form Load event
    void MainForm_Load(object sender, EventArgs e)
    {
        _cts = new CancellationTokenSource();
        _task = DoWorkAsync(_cts.Token);
    }

    // Form Closing event
    void MainForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        _cts.Cancel();
        try
        {
            // if we don't wait here,
            // we may not see "Finished work item N" for the current item,
            // if we do wait, we'll have a deadlock
            _task.Wait();
        }
        catch (Exception ex)
        {
            if (ex is AggregateException)
                ex = ex.InnerException;
            if (!(ex is OperationCanceledException))
                throw;
        }
        MessageBox.Show("Task cancelled");
    }

    // async work
    async Task DoWorkAsync(CancellationToken ct)
    {
        var i = 0;
        while (true)
        {
            ct.ThrowIfCancellationRequested();

            var item = i++;
            await Task.Run(() =>
            {
                Debug.Print("Starting work item " + item);
                // use Sleep as a mock for some atomic operation which cannot be cancelled
                Thread.Sleep(1000); 
                Debug.Print("Finished work item " + item);
            }, ct);
        }
    }
}

That happens because the UI thread's message loop has to continue pumping messages, so the asynchronous continuation inside DoWorkAsync (which is scheduled on the thread's WindowsFormsSynchronizationContext ) has a chance to be executed and eventually have reached the cancelled state. 发生这种情况是因为UI线程的消息循环必须继续泵送消息,因此DoWorkAsync (在线程的WindowsFormsSynchronizationContext上安排)内部的异步继续有机会被执行,并最终达到取消状态。 However, the pump is blocked with _task.Wait() , which leads to the deadlock. 但是,泵被_task.Wait()阻塞,从而导致死锁。 This example is specific to WinForms, but the problem is relevant in the context of WPF, too. 此示例特定于WinForms,但该问题也与WPF有关。

In this case, I don't see any other solutions but to organize a nested message loop, while waiting for the _task . 在这种情况下,我没有其他解决方案,而是在等待_task同时组织嵌套的消息循环。 In a distant way, it is similar to Thread.Join , which keeps pumping messages while waiting for a thread to terminate. 在某种程度上,它类似于Thread.Join ,它在等待线程终止时保持泵送消息。 The framework doesn't seem to offer an explicit task API for this, so I've eventually come up with the following implementation of WaitWithDoEvents : 该框架似乎没有为此提供明确的任务API,因此我最终提出了WaitWithDoEvents的以下实现:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WinformsApp
{
    public partial class MainForm : Form
    {
        CancellationTokenSource _cts;
        Task _task;

        // Form Load event
        void MainForm_Load(object sender, EventArgs e)
        {
            _cts = new CancellationTokenSource();
            _task = DoWorkAsync(_cts.Token);
        }

        // Form Closing event
        void MainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            // disable the UI
            var wasEnabled = this.Enabled; this.Enabled = false;
            try
            {
                // request cancellation
                _cts.Cancel();
                // wait while pumping messages
                _task.AsWaitHandle().WaitWithDoEvents();
            }
            catch (Exception ex)
            {
                if (ex is AggregateException)
                    ex = ex.InnerException;
                if (!(ex is OperationCanceledException))
                    throw;
            }
            finally
            {
                // enable the UI
                this.Enabled = wasEnabled;
            }
            MessageBox.Show("Task cancelled");
        }

        // async work
        async Task DoWorkAsync(CancellationToken ct)
        {
            var i = 0;
            while (true)
            {
                ct.ThrowIfCancellationRequested();

                var item = i++;
                await Task.Run(() =>
                {
                    Debug.Print("Starting work item " + item);
                    // use Sleep as a mock for some atomic operation which cannot be cancelled
                    Thread.Sleep(1000); 
                    Debug.Print("Finished work item " + item);
                }, ct);
            }
        }

        public MainForm()
        {
            InitializeComponent();
            this.FormClosing += MainForm_FormClosing;
            this.Load += MainForm_Load;
        }
    }

    /// <summary>
    /// WaitHandle and Task extensions
    /// by Noseratio - https://stackoverflow.com/users/1768303/noseratio
    /// </summary>
    public static class WaitExt
    {
        /// <summary>
        /// Wait for a handle and pump messages with DoEvents
        /// </summary>
        public static bool WaitWithDoEvents(this WaitHandle handle, CancellationToken token, int timeout)
        {
            if (SynchronizationContext.Current as System.Windows.Forms.WindowsFormsSynchronizationContext == null)
            {
                // https://stackoverflow.com/a/19555959
                throw new ApplicationException("Internal error: WaitWithDoEvents must be called on a thread with WindowsFormsSynchronizationContext.");
            }

            const uint EVENT_MASK = Win32.QS_ALLINPUT;
            IntPtr[] handles = { handle.SafeWaitHandle.DangerousGetHandle() };

            // track timeout if not infinite
            Func<bool> hasTimedOut = () => false;
            int remainingTimeout = timeout;

            if (timeout != Timeout.Infinite)
            {
                int startTick = Environment.TickCount;
                hasTimedOut = () =>
                {
                    // Environment.TickCount wraps correctly even if runs continuously 
                    int lapse = Environment.TickCount - startTick;
                    remainingTimeout = Math.Max(timeout - lapse, 0);
                    return remainingTimeout <= 0;
                };
            }

            // pump messages
            while (true)
            {
                // throw if cancellation requested from outside
                token.ThrowIfCancellationRequested();

                // do an instant check
                if (handle.WaitOne(0)) 
                    return true;

                // pump the pending message
                System.Windows.Forms.Application.DoEvents();

                // check if timed out
                if (hasTimedOut())
                    return false;

                // the queue status high word is non-zero if a Windows message is still in the queue
                if ((Win32.GetQueueStatus(EVENT_MASK) >> 16) != 0) 
                    continue;

                // the message queue is empty, raise Idle event
                System.Windows.Forms.Application.RaiseIdle(EventArgs.Empty);

                if (hasTimedOut())
                    return false;

                // wait for either a Windows message or the handle
                // MWMO_INPUTAVAILABLE also observes messages already seen (e.g. with PeekMessage) but not removed from the queue
                var result = Win32.MsgWaitForMultipleObjectsEx(1, handles, (uint)remainingTimeout, EVENT_MASK, Win32.MWMO_INPUTAVAILABLE);
                if (result == Win32.WAIT_OBJECT_0 || result == Win32.WAIT_ABANDONED_0)
                    return true; // handle signalled 
                if (result == Win32.WAIT_TIMEOUT)
                    return false; // timed out
                if (result == Win32.WAIT_OBJECT_0 + 1) // an input/message pending
                    continue;
                // unexpected result
                throw new InvalidOperationException();
            }
        }

        public static bool WaitWithDoEvents(this WaitHandle handle, int timeout)
        {
            return WaitWithDoEvents(handle, CancellationToken.None, timeout);
        }

        public static bool WaitWithDoEvents(this WaitHandle handle)
        {
            return WaitWithDoEvents(handle, CancellationToken.None, Timeout.Infinite);
        }

        public static WaitHandle AsWaitHandle(this Task task)
        {
            return ((IAsyncResult)task).AsyncWaitHandle;
        }

        /// <summary>
        /// Win32 interop declarations
        /// </summary>
        public static class Win32
        {
            [DllImport("user32.dll")]
            public static extern uint GetQueueStatus(uint flags);

            [DllImport("user32.dll", SetLastError = true)]
            public static extern uint MsgWaitForMultipleObjectsEx(
                uint nCount, IntPtr[] pHandles, uint dwMilliseconds, uint dwWakeMask, uint dwFlags);

            public const uint QS_KEY = 0x0001;
            public const uint QS_MOUSEMOVE = 0x0002;
            public const uint QS_MOUSEBUTTON = 0x0004;
            public const uint QS_POSTMESSAGE = 0x0008;
            public const uint QS_TIMER = 0x0010;
            public const uint QS_PAINT = 0x0020;
            public const uint QS_SENDMESSAGE = 0x0040;
            public const uint QS_HOTKEY = 0x0080;
            public const uint QS_ALLPOSTMESSAGE = 0x0100;
            public const uint QS_RAWINPUT = 0x0400;

            public const uint QS_MOUSE = (QS_MOUSEMOVE | QS_MOUSEBUTTON);
            public const uint QS_INPUT = (QS_MOUSE | QS_KEY | QS_RAWINPUT);
            public const uint QS_ALLEVENTS = (QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY);
            public const uint QS_ALLINPUT = (QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY | QS_SENDMESSAGE);

            public const uint MWMO_INPUTAVAILABLE = 0x0004;

            public const uint WAIT_TIMEOUT = 0x00000102;
            public const uint WAIT_FAILED = 0xFFFFFFFF;
            public const uint INFINITE = 0xFFFFFFFF;
            public const uint WAIT_OBJECT_0 = 0;
            public const uint WAIT_ABANDONED_0 = 0x00000080;
        }
    }
}

I believe the described scenario ought to be pretty common for the UI apps, yet I have found very little material on this subject. 我相信所描述的场景对于UI应用程序应该是很普遍的,但是我发现关于此主题的材料很少。 Ideally, the background task process should be designed in the way it doesn't require a message pump to support synchronous cancellation , but I don't think this is always possible. 理想情况下,应该以不需要消息泵支持同步取消的方式设计后台任务过程 ,但是我认为这并非总是可能的。

Am I missing something? 我想念什么吗? Are there other, perhaps more portable ways/patterns to deal with it? 还有其他也许更便携的方式来处理它吗?

I disagree that it's a code smell to issue a cancellation request without waiting for the cancellation to take effect. 我不同意发出取消请求而不等待取消生效是一种代码味道。 Most of the time, waiting isn't necessary. 大多数时候,不需要等待。

In fact, in UI scenarios, I'd say that's the common approach. 实际上,在UI场景中,我会说这是常见的方法。 If you need to avoid side effects (eg, debug prints, or more realistically, IProgress<T>.Report or a return statement), then simply insert an explicit check for cancellation before performing them: 如果您需要避免副作用(例如,调试打印,或更实际地说是IProgress<T>.Reportreturn语句),则只需在执行它们之前插入一个显式检查是否取消即可:

Debug.Print("Starting work item " + item);
// use Sleep as a mock for some atomic operation which cannot be cancelled
Thread.Sleep(10000);
ct.ThrowIfCancellationRequested();
Debug.Print("Finished work item " + item);

This is particularly useful in a UI context because there's no race conditions around cancellation. 这在UI上下文中特别有用,因为取消周围没有竞争条件。

So we don't want to be doing a synchronous wait as that would be blocking the UI thread, and also possibly deadlocking. 因此,我们不想进行同步等待,因为那样会阻塞UI线程,甚至可能导致死锁。

The problem with handling it asynchronously is simply that the form will be closed before you're "ready". 异步处理它的问题很简单,就是在您“准备就绪”之前将关闭表单。 That can be fixed; 那可以解决; simply cancel the form closing if the asynchronous task isn't done yet, and then close it again "for real" when the task does finish. 简单地取消关闭形式,如果异步任务尚未完成,然后关闭它再次“真正”的时候,任务确实完成。

The method can look something like this (error handling omitted): 该方法看起来像这样(省略了错误处理):

void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
    if (!_task.IsCompleted)
    {
        e.Cancel = true;
        _cts.Cancel();
        _task.ContinueWith(t => Close(), 
            TaskScheduler.FromCurrentSynchronizationContext());
    }
}

Note that, to make the error handling easier, you could at this point make the method async as well, instead of using explicit continuations. 请注意,为了使错误处理更容易,您现在可以使方法也async ,而不是使用显式延续。

Inspired by @Servy's answer , here is another idea: show a temporary modal dialog with a "Please wait..." message, and utilize its modal message loop to wait asynchronously for the pending task. 受到@Servy答案的启发,这是另一个想法:显示带有“请稍候...”消息的临时模态对话框,并利用其模态消息循环异步等待挂起的任务。 The dialog automatically disappears when the task has been fully cancelled. 完全取消任务后,对话框自动消失。

That's what ShowModalWaitMessage does below, called from MainForm_FormClosing . 这就是ShowModalWaitMessage在下面所做的,从MainForm_FormClosing调用。 I think this approach is a bit more user-friendly. 我认为这种方法更加人性化。

等待对话框

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WinformsApp
{
    public partial class MainForm : Form
    {
        CancellationTokenSource _cts;
        Task _task;

        // Form Load event
        void MainForm_Load(object sender, EventArgs e)
        {
            _cts = new CancellationTokenSource();
            _task = DoWorkAsync(_cts.Token);
        }

        // Form Closing event
        void MainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            ShowModalWaitMessage();
        }

        // Show a message and wait
        void ShowModalWaitMessage()
        {
            var dialog = new Form();

            dialog.Load += async (s, e) =>
            {
                _cts.Cancel();

                try
                {
                    // show the dialog for at least 2 secs
                    await Task.WhenAll(_task, Task.Delay(2000));
                }
                catch (Exception ex)
                {
                    while (ex is AggregateException)
                        ex = ex.InnerException;
                    if (!(ex is OperationCanceledException))
                        throw;
                }

                dialog.Close();
            };

            dialog.ShowIcon = false; dialog.ShowInTaskbar = false;
            dialog.FormBorderStyle = FormBorderStyle.FixedToolWindow;
            dialog.StartPosition = FormStartPosition.CenterParent;
            dialog.Width = 160; dialog.Height = 100;

            var label = new Label();
            label.Text = "Closing, please wait...";
            label.AutoSize = true;
            dialog.Controls.Add(label);

            dialog.ShowDialog();
        }

        // async work
        async Task DoWorkAsync(CancellationToken ct)
        {
            var i = 0;
            while (true)
            {
                ct.ThrowIfCancellationRequested();

                var item = i++;
                await Task.Run(() =>
                {
                    Debug.Print("Starting work item " + item);
                    // use Sleep as a mock for some atomic operation which cannot be cancelled
                    Thread.Sleep(1000);
                    Debug.Print("Finished work item " + item);
                }, ct);
            }
        }

        public MainForm()
        {
            InitializeComponent();
            this.FormClosing += MainForm_FormClosing;
            this.Load += MainForm_Load;
        }
    }
}

How about using the older way: 如何使用较旧的方法:

    public delegate void AsyncMethodCaller(CancellationToken ct);

    private CancellationTokenSource _cts;
    private AsyncMethodCaller caller;
    private IAsyncResult methodResult;

    // Form Load event
    private void MainForm_Load(object sender, EventArgs e)
    {
        _cts = new CancellationTokenSource();

        caller = new AsyncMethodCaller(DoWorkAsync);
        methodResult = caller.BeginInvoke(_cts.Token,
            ar =>
            {

            },
            null);

    }

    // Form Closing event
    private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        _cts.Cancel();          
        MessageBox.Show("Task cancellation requested");    
    }

    // async work
    private void DoWorkAsync(CancellationToken ct)
    {
        var i = 0;
        while (true)
        {
            var item = i++;

            Debug.Print("Starting work item " + item);
            // use Sleep as a mock for some atomic operation which cannot be cancelled
            Thread.Sleep(10000);
            Debug.Print("Finished work item " + item);

            if (ct.IsCancellationRequested)
            {
                return;
            }
        }
    }


    private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
    {
        methodResult.AsyncWaitHandle.WaitOne();
        MessageBox.Show("Task cancelled");
    }

You can do some further modification to keep user busy with a nice animation 您可以做一些进一步的修改以使用户忙于制作精美的动画

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM