locked
Is there a pattern for calling an async method synchronously

    Question

  • Following is what I feel may become a common pattern which leads to programmer straight into a deadlock.  I will begin with a "simplest possible example."  I will then motivate the example with the situation it came from.  I wonder if there is an appropriate pattern for what I am trying to do.

    1. Create a new C# 4.0 WPF application project;
    2. Add a reference to the AsyncCtpLibrary
    3. In Mainwindow.cs replace the contents of the MainWindow class with the following code:
    public MainWindow()
    {
      InitializeComponent();
      Loaded += OnLoaded;
    }

     

     

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
     
    // MethodAsync();    // -- if we switch the commented out lines all works
     
    MethodAsync().Wait();
    }

     

     

    private async Task MethodAsync()
    {
     
    await TaskEx.Delay(500);
     
    this.Title = "Done";
    }

     

     

    upon execution this program shows a window with the title MainWindow and promptly hangs.  The reason is obvious.  The UI thread blocks at the wait call until the completion of the task from MethodAsync.  Unfortunately the method never completes because its continuation (starting at this.Title = "Done") will never be scheduled on the UI thread that is presently blocked by the Wait call.  This is a classic deadlock.  (Except its a single thread deadlocking itself, which is theoretically fun at least.)

    Now let me motivate this with a real example.

    I write an application that I want to save data from asynchronously.  I have a very fast method that can "snapshot" the present state and then write it out to disk asynchronously.  I do this on a timer and very much want this not to block the UI.  Async looks like the technology of the day.

    The problem is that I then have to override OnClosing.  If the user then elects to save their work and exit I need to synchronously save the file and then return an appropriate value to signal that the window can close.  I really have to make that call synchronous, because I don't want WPF tearing down the window until my file is safely written out to disk.  (The IO could fail; I may need my UI to tell the user about it.  Furthermore, I don't want the window to disappear before the app is done working.)

    I cannot find a way arround this without offloading the entire save operation to the threadpool.  (Threadpool may be necessary unless I can find a way to pump messages during the Wait().)  In any event, I think this is a common pattern and a well worn and obvious path to the pit of success is warrented.  If it is there, then I have missed it.

     

    In summary the final question is:

    Can a consumer of an arbitrary async method (potentially one he or she does own or control) call that method from the UI thread in a synchronous fashion and predictably avoid deadlock?

    Wednesday, November 03, 2010 5:26 AM

Answers

  • I think I found a better aolution to this problem.  It looks like this:

     

     

    public static class AsyncInline
    {
      public static void Run(Func<Task> item)
      {
        var oldContext = SynchronizationContext.Current;
       
    var synch = new ExclusiveSynchronizationContext();
       
    SynchronizationContext.SetSynchronizationContext(synch);
        synch.Post(
    async _ =>
        {
         
    try
         
    {
            
    await item();
          }
         
    finally
         
    {
            synch.EndMessageLoop();
          }
        },
    null);
        synch.BeginMessageLoop();

     

     

        SynchronizationContext.SetSynchronizationContext(oldContext);
      
    }

     

     

      public static T Run<T>(Func<Task<T>> item)
      {
       
    var oldContext = SynchronizationContext.Current;
       
    var synch = new ExclusiveSynchronizationContext();
       
    SynchronizationContext.SetSynchronizationContext(synch);
        T ret =
    default(T);
        synch.Post(
    async _ =>
        {
         
    try
         
    {
            ret =
    await item();
         
    }
          finally
         
    {
            synch.EndMessageLoop();
          }
        },
    null);
        synch.BeginMessageLoop();
       
    SynchronizationContext.SetSynchronizationContext(oldContext);
       
    return ret;
      }

     

     

      private class ExclusiveSynchronizationContext : SynchronizationContext
     
    {
       
    private bool done;
       
    readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false);
       
    readonly Queue<Tuple<SendOrPostCallback, object>> items =
         
    new Queue<Tuple<SendOrPostCallback, object>>();

        public override void Send(SendOrPostCallback d, object state)
       
    {
              throw new NotSupportedException("We cannot send to our same thread");
          }

     

     

        public override void Post(SendOrPostCallback d, object state)
          {
         
    lock (items)
            {
           
    items.Enqueue(
    Tuple.Create(d, state));
            }
         
    workItemsWaiting.Set();
        
    }

     

     

        public void EndMessageLoop()
       
    {
         
    Post(_ => done =
    true, null);
       
    }

     

     

        public void BeginMessageLoop()
       
    {
           
    while (!done)
         
    {
           
    Tuple<SendOrPostCallback, object> task = null;
            
    lock (items)
           
    {
             
    if (items.Count > 0)
             
    {
               
    task = items.Dequeue();
             
    }
            
    }
            
    if (task != null)
           
    {
             
    task.Item1(task.Item2);
           
    }
           
    else
           
    {
             
    workItemsWaiting.WaitOne();
           
    }
          
    }
        
    }

     

     

         public override SynchronizationContext CreateCopy()
         
    {
            
    return this;
         
    }
       
    }
     
    }

    I like this because it preserves the sematics of synchronous exection.  The method runs exclusively on the calling thread and blocks the calling thread until it is done.

    The above code can definitely be improved, but I think this is a better solution than the ones presented previously.  It would be even better if we could apply it to a task that was already running.

    • Marked as answer by John Melville Saturday, December 11, 2010 5:48 PM
    Saturday, December 11, 2010 5:46 PM

All replies

  • Hi John-

    Blocking the UI thread is invariably a bad idea, as you alluded to.  For your scenario, I think you have a few options.

    The first option is to pump while waiting for the task to complete, which you can do in WPF with a method like:

    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();

    }

    You can then call that method like:

        task.WaitWithPumping();

    and you'll block on that wait call while pumping using WPF's support for spinning up a new message loop (PushFrame).

    Another approach is to prevent the execution of the remainder of your async method on the original synchronization context, offloading to the ThreadPool for example.  You can do that by adding "await TaskScheduler.Default.SwitchTo();" or "await ThreadPoolEx.SwitchTo();" or "await new SynchronizationContext().SwitchTo();" at the beginning of your async method.

    Wednesday, November 03, 2010 5:40 AM
    Moderator
  • Thank you.  I had exact same question.  Guess we all start at same place.  I "kinda" understand reason now, but still a bit confused.  Mine on WinForms here.

    I am still not sure why SwitchTo below is required for Delay (timer).  If TaskEx.Delay returns a Task that starts a Timer.  Then the timer is started and will signal Task to continue on a timer thread?  So the call to "await TaskEx.Delay(2000)" does not start timer before returning Task?  Still a bit confused.  tia

    private async void button1_Click(object sender, EventArgs e)
    {
        //int r = await DelayAsync();  // works
        int r = DelayAsync().Result;  // Does not work unless SwitchTo added below.
    }

    public async Task<int> DelayAsync()
    {
        await TaskScheduler.Default.SwitchTo(); // When blocking on Result from UI thread.
        await TaskEx.Delay(2000);
        Console.WriteLine("Return from delay.");
        return 7;
    }

    Wednesday, November 03, 2010 4:34 PM
  • Hi William-

    By default, when you await a task, we check whether there's a current SynchronizationContext, and if there is, when we resume execution of the async method, we do so by posting back to that SynchronizationContext.  This is why you can write code like:

    ... // access UI controls
    await TaskEx.Delay(2000);
    ... // access UI controls

    because after the await we'll have automatically marshaled you back to the UI thread.  So, yes, the timer is signaling to the task on a ThreadPool thread when the timer fires, but then we're immediately posting back to the UI.  Note that this marshaling behavior can be suppressed using the Task.ConfigureAwait method, e.g.

    ... // access UI controls
    await TaskEx.ConfigureAwait(TaskEx.Delay(2000), false);
    ... // DO NOT access UI controls here, as you're very likely on a ThreadPool thread

    By switching at the beginning of the method to the ThreadPool, when you await, SynchronizationContext.Current is now null (or at least it's not the UI sync context), and thus after the await we simply continue execution whereever the awaited task completed.

    Wednesday, November 03, 2010 4:38 PM
    Moderator
  • I think I found a better aolution to this problem.  It looks like this:

     

     

    public static class AsyncInline
    {
      public static void Run(Func<Task> item)
      {
        var oldContext = SynchronizationContext.Current;
       
    var synch = new ExclusiveSynchronizationContext();
       
    SynchronizationContext.SetSynchronizationContext(synch);
        synch.Post(
    async _ =>
        {
         
    try
         
    {
            
    await item();
          }
         
    finally
         
    {
            synch.EndMessageLoop();
          }
        },
    null);
        synch.BeginMessageLoop();

     

     

        SynchronizationContext.SetSynchronizationContext(oldContext);
      
    }

     

     

      public static T Run<T>(Func<Task<T>> item)
      {
       
    var oldContext = SynchronizationContext.Current;
       
    var synch = new ExclusiveSynchronizationContext();
       
    SynchronizationContext.SetSynchronizationContext(synch);
        T ret =
    default(T);
        synch.Post(
    async _ =>
        {
         
    try
         
    {
            ret =
    await item();
         
    }
          finally
         
    {
            synch.EndMessageLoop();
          }
        },
    null);
        synch.BeginMessageLoop();
       
    SynchronizationContext.SetSynchronizationContext(oldContext);
       
    return ret;
      }

     

     

      private class ExclusiveSynchronizationContext : SynchronizationContext
     
    {
       
    private bool done;
       
    readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false);
       
    readonly Queue<Tuple<SendOrPostCallback, object>> items =
         
    new Queue<Tuple<SendOrPostCallback, object>>();

        public override void Send(SendOrPostCallback d, object state)
       
    {
              throw new NotSupportedException("We cannot send to our same thread");
          }

     

     

        public override void Post(SendOrPostCallback d, object state)
          {
         
    lock (items)
            {
           
    items.Enqueue(
    Tuple.Create(d, state));
            }
         
    workItemsWaiting.Set();
        
    }

     

     

        public void EndMessageLoop()
       
    {
         
    Post(_ => done =
    true, null);
       
    }

     

     

        public void BeginMessageLoop()
       
    {
           
    while (!done)
         
    {
           
    Tuple<SendOrPostCallback, object> task = null;
            
    lock (items)
           
    {
             
    if (items.Count > 0)
             
    {
               
    task = items.Dequeue();
             
    }
            
    }
            
    if (task != null)
           
    {
             
    task.Item1(task.Item2);
           
    }
           
    else
           
    {
             
    workItemsWaiting.WaitOne();
           
    }
          
    }
        
    }

     

     

         public override SynchronizationContext CreateCopy()
         
    {
            
    return this;
         
    }
       
    }
     
    }

    I like this because it preserves the sematics of synchronous exection.  The method runs exclusively on the calling thread and blocks the calling thread until it is done.

    The above code can definitely be improved, but I think this is a better solution than the ones presented previously.  It would be even better if we could apply it to a task that was already running.

    • Marked as answer by John Melville Saturday, December 11, 2010 5:48 PM
    Saturday, December 11, 2010 5:46 PM
  • I get a MethodAccessException when SetSynchronizationContext(synch) executes.  Checking MSDN, the Silverlight documentation for that method very plainly spells out that we shouldn't call this it.

    How, then, can we get this to work?


    Tuesday, August 16, 2011 3:42 AM
  • Sorry, it looks like you're out of luck for silverlight.  The SetSynchronizationContext requires full trust.  MSDN does state that silverlight does not allow apps to call this function.  My example came from a WPF app which has full trust,

    I doubt you will be able work arround this as the SetSynchronizationContext is reallt the "trick" to this code.

    Sorry again,

    John Melville

    Saturday, August 20, 2011 10:58 PM
  • This code has a but -- it swallows exceptions thrown from the called method.  The code below will throw an AggregateException if the passed method throws an exception
     public static class AsyncInline
     {
      public static void Run(Func<Task> item)
      {
       var oldContext = SynchronizationContext.Current;
       var synch = new ExclusiveSynchronizationContext();
       SynchronizationContext.SetSynchronizationContext(synch);
       synch.Post(async _ =>
       {
        try
        {
         await item();
        }
        catch (Exception e)
        {
         synch.InnerException = e;
         throw;
        } finally
        {
         synch.EndMessageLoop();
        }
       }, null); 
       synch.BeginMessageLoop();
       SynchronizationContext.SetSynchronizationContext(oldContext);
      }
      public static T Run<T>(Func<Task<T>> item)
      {
       var oldContext = SynchronizationContext.Current;
       var synch = new ExclusiveSynchronizationContext();
       SynchronizationContext.SetSynchronizationContext(synch);
       T ret = default(T);
       synch.Post(async _ =>
       {
        try
        {
         ret = await
         item();
        } catch (Exception e)
        {
         synch.InnerException = e;
         throw;
        } finally
        {
         synch.EndMessageLoop();
        }
       }, null);
       synch.BeginMessageLoop();
       SynchronizationContext.SetSynchronizationContext(oldContext);
       return ret;
      }
    
      private class ExclusiveSynchronizationContext : SynchronizationContext
      {
       private bool done;
       public Exception InnerException { get; set; }
       readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false);
       readonly Queue<Tuple<SendOrPostCallback, object>> items =
        new Queue<Tuple<SendOrPostCallback, object>>();
    
       public override void Send(SendOrPostCallback d, object state)
       {
        throw new NotSupportedException("We cannot send to our same thread");
       }
       public override void Post(SendOrPostCallback d, object state)
       {
        lock (items)
        {
         items.Enqueue(Tuple.Create(d, state));
        }
        workItemsWaiting.Set();
       }
       public void EndMessageLoop()
       {
        Post(_ => done = true, null);
       }
       public void BeginMessageLoop()
       {
        while (!done)
        {
         Tuple<SendOrPostCallback, object> task = null;
         lock (items)
         {
          if (items.Count > 0)
          {
           task = items.Dequeue();
          }
         }
         if (task != null)
         {
          task.Item1(task.Item2);
          if (InnerException != null) // the method threw an exeption
          {
           throw new AggregateException("AsyncInline.Run method threw an exception.",
            InnerException);
          }
         }
         else
         {
          workItemsWaiting.WaitOne();
         }
        }
       }
       public override SynchronizationContext CreateCopy()
       {
        return this;
       }
      }
     }
    
    

    Thursday, September 01, 2011 1:39 AM