在UI线程上同步取消挂起的任务

时间:2014-01-02 05:03:20

标签: c# .net multithreading task-parallel-library async-await

有时,一旦我请求取消CancellationTokenSource.Cancel的待处理任务,我需要确保任务已正确达到已取消状态,然后才能继续。当应用程序终止并且我想要优雅地取消所有挂起的任务时,我经常遇到这种情况。但是,它也可能是UI工作流规范的要求,当新的后台进程只能在当前挂起的进程被完全取消或自然到达时才开始。

如果有人分享他/她处理这种情况的方法,我会很感激。我说的是以下模式:

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

因此,已知在UI线程上使用时能够容易地导致死锁。但是,并不总是可以使用异步等待(即await task;例如,here 可能的情况之一。同时,简单地请求取消并继续而不实际观察其状态是代码气味。

作为说明问题的简单示例,我可能希望确保在DoWorkAsync事件处理程序中完全取消了以下FormClosing任务。如果我不等_task内的MainForm_FormClosing,我可能甚至看不到当前工作项的"Finished work item N"跟踪,因为该应用终止于待处理的子项中间任务(在池线程上执行)。如果我确实等待,则会导致死锁:

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);
        }
    }
}
发生这种情况是因为UI线程的消息循环必须继续泵送消息,因此DoWorkAsync内的异步延续(在线程的WindowsFormsSynchronizationContext上调度)有机会被执行并最终到达被取消的州。但是,泵被_task.Wait()阻塞,导致死锁。此示例特定于WinForms,但问题也与WPF的上下文相关。

在这种情况下,我没有看到任何其他解决方案,只是在等待_task时组织嵌套的消息循环。在远处,它类似于{ {3}},在等待线程终止时保持消息。该框架似乎没有为此提供明确的任务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;
        }
    }
}

我相信所描述的场景对于UI应用程序来说应该是相当普遍的,但我发现这个主题的材料很少。 理想情况下,后台任务流程的设计应该不需要消息泵来支持同步取消,但我不认为这总是可行的。

我错过了什么吗?是否有其他可能更便携的方式/模式来处理它?<​​/ p>

4 个答案:

答案 0 :(得分:10)

我不同意在不等待取消生效的情况下发出取消请求是一种代码味道。大多数时候,没有必要等待。

事实上,在UI场景中,我会说这是常见的方法。如果您需要避免副作用(例如,调试打印,或更现实地,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);

这在UI上下文中特别有用,因为取消时没有竞争条件。

答案 1 :(得分:10)

所以我们不想做同步等待,因为这会阻塞UI线程,也可能会死锁。

异步处理它的问题只是表单将在“准备好”之前关闭。这可以修复;如果异步任务尚未完成,只需取消表单关闭,然后当任务 完成时再“关闭”表单。

该方法看起来像这样(省略错误处理):

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

请注意,为了简化错误处理,您可以在此时创建方法async,而不是使用显式连续。

答案 2 :(得分:7)

@Servy's answer的启发,这是另一个想法:显示一个带有“Please wait ...”消息的临时模式对话框,并利用其模态消息循环异步等待挂起任务。任务完全取消后,对话框会自动消失。

这是ShowModalWaitMessageMainForm_FormClosing调用的内容。我认为这种方法更加用户友好。

Wait Dialog

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;
        }
    }
}

答案 3 :(得分:0)

如何使用旧方式:

    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");
    }

你可以做一些进一步的修改,让用户忙于一个好的动画