Sometimes, when I asked to cancel a pending task with CancellationTokenSource.Cancel , I need to make the task get to the canceled state before continuing. Most often, I have to face a situation where the application ends, and I want to gracefully cancel all pending tasks. However, this may also be a requirement of the specification of the user interface workflow when a new background process can only be started if the current pending is completely canceled or has ended naturally.
I would be grateful if someone shares his / her approach in solving this situation. I am talking about the following pattern:
_cancellationTokenSource.Cancel(); _task.Wait();
As you know, it is known to be able to cause a deadlock when using a user interface in a stream. However, it is not always possible to use asynchronous wait (i.e. await task , for example, here is one of the cases where this is possible). At the same time, it is the smell of code that simply requires cancellation and continuation without actually monitoring its state.
As a simple example illustrating the problem, I can make sure that the next DoWorkAsync task DoWorkAsync completely canceled inside the FormClosing event FormClosing . If I do not expect _task inside MainForm_FormClosing , I donโt even see the "Finished work item N" trace for the current work item, since the application terminates in the middle of the pending subtask (which runs on the pool thread). If I wait, it will lead to a dead end:
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); } } }
This is because the message loop of the UI thread must continue to send messages, so the asynchronous continuation inside DoWorkAsync (which is scheduled in the WindowsFormsSynchronizationContext thread) has a chance to execute and eventually reached the canceled state. However, the pump is blocked by _task.Wait() , which leads to a dead end. This example applies to WinForms, but the issue is also relevant in the context of WPF.
In this case, I see no other solutions than to organize a nested message loop, waiting for _task . . In a distant way, it is similar to Thread.Join , which continues to pump messages while waiting for the thread to complete. The structure does not seem to offer an explicit task API for this, so I eventually came up with the following WaitWithDoEvents implementation:
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 (eg 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 that the described scenario should be fairly common for user interface applications, but I have found very little material on this subject. Ideally, the background task process should be designed so as not to require the message pump to support synchronous shutdown , but I don't think it is always possible.
Am I missing something? Are there other, possibly more portable ways / patterns to handle this?