ParallelExtensionsExtras Tour - #15 - Specialized Task Waiting

ParallelExtensionsExtras Tour - #15 - Specialized Task Waiting

  • Comments 9

(The full set of ParallelExtensionsExtras Tour posts is available here.)

The Task Parallel Library provides the Task.Wait method, which synchronously waits for the target Task to complete.  If the Task completed successfully, the method simply returns.  If the Task completed due to an unhandled exception or cancellation, Wait throws an appropriate exception to connote that you can't just blindly continue expecting the results or side-effects of the Task to have taken place.

While this is a very useful form of waiting, there are others that can be beneficial in certain situations, and ParallelExtensionsExtras includes a few different forms in the TaskExtrasExtensions.cs file.

WaitWithPumping

In a Windows Presentation Foundation application, especially when doing unit testing, you sometimes need to wait for a task on the UI thread.  However, in doing so you don’t want to block the UI thread, and instead you want to continue the WPF message loop, maintaining a responsive application.  For this purpose, ParallelExtensionsExtras includes the WaitWithPumping extension method for Task.

public static void WaitWithPumping(this Task task)

{

    if (task == null) throw new ArgumentNullException(“task”);

    var nestedFrame = new DispatcherFrame();

    task.ContinueWith(_ => nestedFrame.Continue = false);

    Dispatcher.PushFrame(nestedFrame);

    task.Wait();

}

 

WaitWithPumping enters a message loop that will only exit when the task completes, which WPF knows about through a continuation applied to the task.  Once the message loop has exited, we Wait on the task simply to propagate exceptions in the case where the task did not complete successfully.

With that in place, you could write WPF code like the following:

private void button1_Click(object sender, RoutedEventArgs e)

{

    var t = Task.Factory.StartNew(() => Thread.Sleep(5000));

 

    t.WaitWithPumping(); // UI remains responsive during call

 

    MessageBox.Show(t.Status.ToString()); // will show "RanToCompletion"

}

 

Even thought Task t won’t complete for 5 seconds, and even though WaitWithPumping will block, the UI will still remain responsive.  Then, because we waited for the Task to complete before exiting the message loop, the MessageBox will show “RanToCompletion”.

WaitForCompletionStatus

Sometimes you want to wait for a Task, but you don’t want the Wait operation to throw an exception, even if the target task completed in the Faulted or Canceled states.  To achieve this, ParallelExtensionsExtras provides the WaitForCompletionStatus extension method on Task:

public static TaskStatus WaitForCompletionStatus(this Task task)

{

    if (task == null) throw new ArgumentNullException("task");

    ((IAsyncResult)task).AsyncWaitHandle.WaitOne();

    return task.Status;

}

 

This method relies on the fact that Task implements IAsyncResult, and thus implements the AsyncWaitHandle property.  AsyncWaitHandle returns a WaitHandle that will be set when the task completes, and waiting on this wait handle will not throw exceptions in the same manner as does waiting on the task directly.

WaitForCompletionStatus returns the final TaskStatus of the task that was waited on.  This makes it easy to write code that switches on a task’s completion state in order to do appropriate follow-up processing, e.g.

switch(task.WaitForCompletionStatus)

{

    case TaskStatus.RanToCompletion:

        Console.WriteLine(“Woo hoo!”);

        break;

    case TaskStatus.Faulted:

        Console.WriteLine(“Uh oh: “ + task.Exception.Message);

        break;

    case TaskStatus.Canceled:

        Console.WriteLine(“Oh well.”);

        break;

}

Leave a Comment
  • Please add 4 and 6 and type the answer here:
  • Post
  • Hello,

    Do you have a version of WaitWithPumping that would work with WinForms ?

    I have tried task.Result but it bloks my UI.

    Is there an other way to wait for completion without bloking the UI ?

    Thanks.

  • Hello,

    Do you have a version of WaitWithPumping that would work with WinForms ?

    I have tried task.Result but it bloks my UI.

    Is there an other way to wait for completion without bloking the UI ?

    Thanks.

  • I have a similar question about WaitWithPumping for WinForms - and also a question about dependencies. I could probably answer this by downloading the Extensions and looking at the code, but even so, I'm a bit surprised you didn't answer this in your post: how do you handle the dependency on WPF? If I use the Extensions from a WinForms app (or an ASP.NET app, for that matter), will I be dragging in all the WPF assemblies because of an extension method I'm not going to use? Or did you pull WaitWithPumping into a separate WPF-specific assembly so I won't have to worry about that?

  • First, let me clarify that such a pumping solution should only be considered if you have no other options.  In general, it's best to offload all work from the UI thread and use a callback mechanism to get work back to the UI thread.  You can do this using a variety of mechanisms, e.g. Task, BackgroundWorker, etc.  Waiting while pumping should only be used if you don't have an alternative.  An example of where this might be the case if if you're running a unit test that starts on the UI thread, and your unit test needs to assert the results of the test prior to exiting the method that started the test.  In other words, the test needs to synchronously call a method on the UI thread that kicks off the work and doesn't exit the method call until the work has been completed (if you can avoid such a design/need, do so).

    Now, to the specific questions:

    Task.Result may block if the task hasn't completed.  It should not be used on the UI thread unless you know the Task has already completed.  As stated above, offload the work from the UI, and in the case of Task, use a continuation (i.e. ContinueWith) to complete the operation... you can have that continuation run directly on the UI thread (by passing in the right TaskScheduler), or from within the continuation you could use Control.BeginInvoke to get work back to the UI.

    Regarding Windows Forms, to my knowledge it doesn't have anything equivalent to WPF's DispatcherFrame capabilities.  You could potentially hack something together based on p/invoking out to MsgWaitForMultipleObjects, e.g. the following might work ok:

    internal void WaitWithPumping(Task t)

    {

       BeginInvoke((MethodInvoker)delegate { Focus(); });

       t.ContinueWith(_ => BeginInvoke((MethodInvoker)delegate { }));

       while (!t.IsCompleted)

       {

           const int QS_ALLINPUT = 0x04FF;

           const int MWMO_INPUTAVAILABLE = 0x0004;

           Application.DoEvents();

           MsgWaitForMultipleObjectsEx(0, IntPtr.Zero, Timeout.Infinite, QS_ALLINPUT, MWMO_INPUTAVAILABLE);

       }

    }

    [DllImport("user32.dll")]

    private static extern int MsgWaitForMultipleObjectsEx(

       int nCount, IntPtr pHandles, int dwMilliseconds, int dwWakeMask, int dwFlags);

    Regarding dependencies, right now ParallelExtensionsExtras takes a dependency on WindowsBase.dll.  However, this DLL won't be loaded until it's needed, which won't happen until the WaitForPumping method is called causing it to be JIT compiled and the relevant assembly to be needed.  If you don't call the method, you won't need the assembly (assuming nothing else in your app needs it). You can verify this in the debugger's Modules window.

    I haven't tried doing so, but it's also possible you could just use the WPF-based WaitWhilePumping method as-is from Windows Forms.  They're both based on the same underlying concepts around maintaining a Windows message loop.

  • I've been trying to build an equivalent 'WaitAllWithPumping' but not sure how to make it work without a 'ContinueAllWith'.  Any ideas?

  • It would end up being almost identical:

    public static void WaitAllWithPumping(

       params Task [] tasks)

    {

       if (tasks == null) throw new ArgumentNullException(“tasks”);

       var nestedFrame = new DispatcherFrame();

       Task.Factory.ContinueWhenAll(tasks, _ => nestedFrame.Continue = false);

       Dispatcher.PushFrame(nestedFrame);

       Task.WaitAll(tasks);

    }

    I hope that helps.

  • Awesome, thanks!  The ContinueWhenAll was exactly what I needed, I was looking on Task instead of Factory.

    By the way, your Document 'Patterns_of_Parallel_Programming' has proven invaluable in my programming efforts.  It's concise, extremely well written, and full of useful and relevant examples.

  • Hi c# Programmer-

    Excellent.  I'm glad that worked for you, and it's great to hear the patterns document has been useful to you!  Thanks for letting me know.

  • Before the .NET 4.0 framework I used this (and still do) to keep the UI thread alive while running length tasks:

    ''' <summary>

     ''' Wait for call to complete while pumping, allowing the UI thread to still perform some processing.

     ''' Call the delegate EndInvoke method to retrieve the result after this method.

     ''' We start with a very small sleep interval and increase it as the task takes more time,

     ''' this way short tasks are not slowed down too much, and the number of DoEvents is not too big either.

     ''' Still a typical overhead is 0.1 to 0.2 seconds!

     ''' </summary>

     ''' <param name="result"></param>

     ''' <remarks></remarks>

    Public Shared Sub WaitWhilePumping(ByVal result As IAsyncResult)

       Dim sleep As Integer = 8, deltaSleep As Integer = 8, maxSleep As Integer = 128, doEventsInterval As Integer = 512

       For Each frm As Form In System.Windows.Forms.Application.OpenForms

         frm.Cursor = Cursors.WaitCursor

       Next

       Dim elapsedTime As Integer = 0

       While result.IsCompleted = False

         System.Threading.Thread.Sleep(sleep)

         elapsedTime += sleep

         'Only run DoEvents every doEventsInterval, it's a costly call

         If elapsedTime > doEventsInterval Then

           Application.DoEvents()

           elapsedTime = 0

         End If

         'Increase sleep timer.

         If sleep < maxSleep Then sleep += deltaSleep

       End While

       For Each frm As Form In System.Windows.Forms.Application.OpenForms

         frm.Cursor = Cursors.Default

       Next

     End Sub

    The IAsyncResult is returned using BeginIvoke on a delegate. I will rewrite this for 4.0 using tasks, but I am still learning ...

    Yves

Page 1 of 1 (9 items)