Cancel a pending task synchronously in a user interface thread

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?

+19
multithreading c # task-parallel-library async-await
Jan 02 '14 at 5:03
source share
4 answers

Thus, we do not want to do synchronous wait, as this will block the user interface thread, as well as possibly deadlock.

The problem with asynchronous processing is that the form will close until you are ready. It can be fixed; just undo the closing of the form if the asynchronous task has not yet been completed, and then close it again โ€œfor realโ€ when the task is completed.

The method may look something like this (processing error 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 facilitate error handling at this point, you can make an async method rather than using explicit extensions.

+8
Jan 02 '14 at 15:52
source share

I do not agree that it is the smell of the code to issue a cancellation request without waiting for the cancellation to be canceled. Most of the time you do not need to wait.

In fact, in user interface scripts, I would say that a common approach. If you need to avoid side effects (e.g. debugging prints or more realistically, IProgress<T>.Report or the return ), just add an explicit undo check before executing them:

 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 especially useful in the context of the user interface because there are no race conditions around cancellation.

+9
Jan 02 '14 at 11:18
source share

Inspired by @Servy's answer , here's another idea: show a temporary modal dialog with the message โ€œWait ...โ€ and use its modal message loop to asynchronously wait for a pending task. The dialog automatically disappears when the task is completely canceled.

What is below MainForm_FormClosing called from MainForm_FormClosing . I think this approach is more user friendly.

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; } } } 
+6
Jan 02 '14 at 20:51
source share

How to use the older method:

  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 make another modification so that the user is busy with pleasant animation

0
Jan 2 '14 at 10:59
source share



All Articles