簡體   English   中英

在UI線程上同步取消待處理的任務

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

有時,一旦我使用CancellationTokenSource.Cancel請求取消了待處理的任務,我需要確保任務已正確達到canceled狀態 ,然后才能繼續。 當應用程序終止時,我通常會遇到這種情況,而我想優雅地取消所有待處理的任務。 但是,這也可能是UI工作流規范的要求,當新的后台進程僅在當前的未決進程已完全取消或自然結束時才能啟動。

如果有人分享他/她的方式來處理這種情況,我將不勝感激。 我正在談論以下模式:

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

照原樣,眾所周知,當在UI線程上使用時,能夠輕松導致死鎖。 然而,它並不總是可以使用一個異步等待,而不是(即await task ;例如, 這里是當它可能的情形之一)。 同時,簡單地請求取消並繼續而不實際觀察其狀態是一種代碼味道。

作為說明問題的簡單示例,我可能要確保在FormClosing事件處理程序中完全取消了以下DoWorkAsync任務。 如果我不等待MainForm_FormClosing內的_task ,我可能甚至看不到當前工作項的"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同時組織嵌套的消息循環。 在某種程度上,它類似於Thread.Join ,它在等待線程終止時保持泵送消息。 該框架似乎沒有為此提供明確的任務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應用程序應該是很普遍的,但是我發現關於此主題的材料很少。 理想情況下,應該以不需要消息泵支持同步取消的方式設計后台任務過程 ,但是我認為這並非總是可能的。

我想念什么嗎? 還有其他也許更便攜的方式來處理它嗎?

我不同意發出取消請求而不等待取消生效是一種代碼味道。 大多數時候,不需要等待。

實際上,在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上下文中特別有用,因為取消周圍沒有競爭條件。

因此,我們不想進行同步等待,因為那樣會阻塞UI線程,甚至可能導致死鎖。

異步處理它的問題很簡單,就是在您“准備就緒”之前將關閉表單。 那可以解決; 簡單地取消關閉形式,如果異步任務尚未完成,然后關閉它再次“真正”的時候,任務確實完成。

該方法看起來像這樣(省略了錯誤處理):

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

請注意,為了使錯誤處理更容易,您現在可以使方法也async ,而不是使用顯式延續。

受到@Servy答案的啟發,這是另一個想法:顯示帶有“請稍候...”消息的臨時模態對話框,並利用其模態消息循環異步等待掛起的任務。 完全取消任務后,對話框自動消失。

這就是ShowModalWaitMessage在下面所做的,從MainForm_FormClosing調用。 我認為這種方法更加人性化。

等待對話框

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

如何使用較舊的方法:

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

您可以做一些進一步的修改以使用戶忙於制作精美的動畫

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM