Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cancelling a pending task synchronously on the UI thread

Tags:

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. 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.

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. However, it is not always possible to use an asynchronous wait instead (i.e. await task; e.g., here is one of the cases when it is possible). 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. 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). 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. However, the pump is blocked with _task.Wait(), which leads to the deadlock. This example is specific to WinForms, but the problem is relevant in the context of WPF, too.

In this case, I don't see any other solutions but to organize a nested message loop, while waiting for the _task. In a distant way, it is similar to Thread.Join, which keeps pumping messages while waiting for a thread to terminate. 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:

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. 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?

like image 833
noseratio Avatar asked Jan 02 '14 05:01

noseratio


1 Answers

So we don't want to be doing a synchronous wait as that would be blocking the UI thread, and also possibly deadlocking.

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.

like image 152
Servy Avatar answered Sep 19 '22 17:09

Servy