Locked Async Yield seems to be broken

  • Wednesday, February 08, 2012 8:32 PM
     
      Has Code

    Hi. I'm afraid I'm getting odd behaviour using the TaskEx.Yield function. I have code similar to the following:

    ASync Function LongRunningOperationAsync()
       For Each Item in SomeList
         DoSomething()
         Await TaskEx.Yield
       Next
    End Function

    However, while this function executes, all GUI events grind to a halt!

    In short, Yield doesn't do what it says on the tin. If I use SynchronizationContext.Post() to post additional work items, they execute during a Yield as expected; but standard Windows events (like mousemove, click etc) are put on hold until the async function exits. Which is just stupid.

    If I replace TaskEx.Yield with TaskEx.Delay(1) or Application.DoEvents, the events are processed correctly; although both of these solutions have their drawbacks (mainly in terms of performance). I don't want to start a new message pump, and I don't want to delay for any specific length of time (because that slows the program down). I just want to Yield to the message queue, and pick up the work again afterwards, in precisely the manner that TaskEx.Yield is supposed to perform.

    I have an example program that demonstrates the issue, if anyone wants it. I don't see any way to upload it to this forum, however.

All Replies

  • Thursday, February 09, 2012 4:58 PM
    Moderator
     
     Proposed Answer

    Hi ChewsHuman-

    The issue is that Task.Yield targets the current SynchronizationContext's Post, and the WPF derivation of SynchronizationContext (DispatcherSynchronizationContext) in .NET 4 implements Post to target DispatcherPriority.Normal, which is of a higher priority than those GUI events you're referring to, and for backwards compatibility reasons that can't be changed.  However, in .NET 4.5, DispatcherSynchronizationContext has been augmented such that if you use one of the new InvokeAsync APIs and pass in a DispatcherPriority, that priority will be used for the DispatcherSynchronizationContext that's current while executing the provided delegate.  So, while the following will freeze the UI as you've found:

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            while (true)
            {
                await Task.Yield();
            }
        }

    if you instead wrote the following, the UI will remain responsive as Input, Rendering, and other such priorities will take precedence:

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Dispatcher.InvokeAsync(async delegate
            {
                while (true)
                {
                    await Task.Yield();
                }
            }, DispatcherPriority.Background);
        }

    I hope that helps.

  • Thursday, February 09, 2012 5:49 PM
     
     

    Hi Stephen, and thanks for getting back to me.

    Unfortunately, however, I am not using WPF - I'm using Windows Forms - and therefore have no Dispatcher to call InvokeAsync() on. Besides, even if I did, surely you agree that this is a very clunky solution? The whole point of having support for void async functions is to handle GUI events; so to force developers to wrap all async functionality in an InvokeAsync() call, simply to make Yield() work like it's supposed to, is just silly.

    Surely Yield() ought to yield to GUI events if it is called on the GUI thread? I mean, under what circumstances would anyone not want this?

    Incidentally, you mentioned that InvokeAsync() was a .NET4.5 call. Correct me if I'm wrong, but isn't that only available in Visual Studio 2012, which has not yet been officially released? I have 2010 with SP1 and the ASync CTP 3. How can I get Yield() to work properly in this environment?

    Cheers,
       Stuart.

  • Friday, February 10, 2012 4:24 PM
    Moderator
     
     Proposed Answer

    Hi Stuart-

    I agree it's not ideal, and we've been discussing whether there's anything we can do about this for Task.Yield.

    For Windows Forms, I don't know of a good solution.  That goes beyond await: Windows Forms only provides the Control.BeginInvoke mechanism to marshal back to the UI thread, and that mechanism doesn't allow you to control the priority of such marshaled delegates with regards to input and rendering events.  For Windows Forms, you might be stuck with Application.DoEvents(), which has its own caveats.  Or "await Task.Delay(1)" as you suggested, which will work but likely with 10 to 15 milliseconds of delay.  Or "await Task.Run(() => {});" might help as well.

    If you were using WPF, you could write a custom awaiter that would let you do exactly what you wanted and more.  Here's an example:

    public static class WpfExtensions
    {
        public static DispatcherPriorityAwaiter GetAwaiter(this DispatcherPriority priority)
        {
            return new DispatcherPriorityAwaiter(priority);
        }

        public struct DispatcherPriorityAwaiter
        {
            private readonly DispatcherPriority m_priority;

            public DispatcherPriorityAwaiter(DispatcherPriority priority) { m_priority = priority; }
            public bool IsCompleted { get { return false; } }
            public void GetResult() { }
            public void OnCompleted(Action continuation)
            {
                Dispatcher.CurrentDispatcher.InvokeAsync(continuation, m_priority); // could use BeginInvoke on .NET 4
            }
        }
    }

    That would allow you to write my same example as:

    while(true)
    {
        await DispatcherPriority.Background;
    }

    I hope that helps.

  • Friday, February 10, 2012 4:32 PM
     
     

    For Windows Forms, I don't know of a good solution.  That goes beyond await: Windows Forms only provides the Control.BeginInvoke mechanism to marshal back to the UI thread, and that mechanism doesn't allow you to control the priority of such marshaled delegates with regards to input and rendering events.  For Windows Forms, you might be stuck with Application.DoEvents(), which has its own caveats.  Or "await Task.Delay(1)" as you suggested, which will work but likely with 10 to 15 milliseconds of delay.  Or "await Task.Run(() => {});" might help as well.

    Hi Stephen, and thanks for getting back to me.

    So what you're basically saying, then, in a nutshell, is Yield doesn't work in Windows Forms applications.

    So what's the point? Without Yield(), there's no efficient way to write an async function on the GUI thread. And if you're going to use a worker thread, you might as well just do it synchronously. So why use Async at all?

    In short, until this is fixed, the entire Async framework is completely useless. Which is a shame. I was hoping to make heavy use of Async in my applications, even though writing interruptable operations was a complete pain in the backside, with all these CancellationTokens I have to keep passing around from function to function, instead of the simple convenience of a Thread.Abort() call.

    Should I expect a fix, or should I just give up on Async? Because Delay(1) isn't going to cut it.

    Cheers,
       Stuart

  • Friday, February 10, 2012 4:58 PM
    Moderator
     
     

    Hi Stuart-

    I'm saying that Windows Forms doesn't provide any built-in mechanism that I know of to marshal a delegate to the UI thread at a lower priority than other Windows messages.  And without such a mechanism, no, I would not expect Task.Yield to provide the capability you need, since it can really only layer on top of mechanisms in the underlying frameworks (and the only way it has to do that generically today is via SynchronizationContext.Post, and Windows Form's SynchronizationContext.Post is just a wrapper for Control.BeginInvoke).  You're using .NET 4, so even if this could be improved in the future, I personally doubt any such improvement would be backported to .NET 4.  It's possible you could cook up your own solution via custom Windows messages and message hooks or something like that, but I've never tried.

    Have you tried "await Task.Run(() => {});"?  It's not perfect, but it will temporarily get off of the UI thread long enough for a task to be processed on the ThreadPool and then post back to the UI, which could be good enough in your case to allow any pending events to be processed.  You could even stick some waiting or spinning into the body of the Run delegate if you found that a longer period of delay was better suited to your app's particular characteristics.

  • Friday, February 10, 2012 6:18 PM
     
     

    I'm saying that Windows Forms doesn't provide any built-in mechanism that I know of to marshal a delegate to the UI thread at a lower priority than other Windows messages.

    Ah, but I don't require it to be marshalled at a lower priority. I thought BeginInvoke() just sent a windows message to the underlying control? In which case, it should be added to the end of the queue, behind any other pending messages. This would be fine.

    What we see, however, is that it somehow gets added to the front of the message queue, ahead of clicks and such, but behind other pending posted work. This makes no sense.

    To do it manually - that is, to "cook up my own solution" negates all advantages of using Async; as the functions would, once again, have to be peppered with ugly Invokes and delegates. And if you're going to do that, you might as well just use a background thread and be done with it.

    I appreciate you taking the time to respond to my issue, and really like the idea of Async; but as things stand, Async is broken and unusable. It needs to be fixed. You seem to be implying that it won't be.

    Isn't this the whole point of the CTP? To find out what works and what doesn't? It's in beta! Isn't this exactly the right time to address issues of this nature?

  • Friday, February 10, 2012 6:31 PM
    Moderator
     
     Proposed Answer

    See the answer from Hans Passant at http://stackoverflow.com/questions/6789105/winforms-message-loop-not-responsive for more details on how Control.BeginInvoke actually works.  It doesn't work exactly as you expect it to.

    Also note that cooking up a custom solution does not negate benefits of async/await.  It just means you need to implement once behind the facade of an awaiter whatever the right mechanism is.  Note that in my previous response, I highlighted how a custom awaiter could be implemented on top of WPF's Dispatcher... you do that once, and then you get the full flexibility of async/await, while taking advantage of your custom solution.  It does not mean you end up with "ugly Invokes and delegates" throughout your code.  Again, I've never tried such an approach with Windows Forms, I'm just saying it's something you could consider.

    I disagree that async "is broken and unusable."  You have a very specific scenario, that of taking a long running compute-bound piece of work that must be run on the UI thread and breaking it up into discrete pieces, and yes, async/await is not providing the support you need to do that in Windows Forms on .NET 4, I agree.  But, async/await is very useful and usuable for many, many other purposes beyond this specific one.

    And, yes, one of the key points of a CTP is to gather feedback.  We sincerely appreciate your feedback, we've heard it, we understand your concerns, and as I wrote in my response above, we've been discussing it.

    Thanks, again, for taking the time to share your thoughts.


  • Friday, February 10, 2012 6:40 PM
     
     
    I disagree that async "is broken and unusable."  You have a very specific scenario, that of taking a long running compute-bound piece of work that must be run on the UI thread and breaking it up into discrete pieces

    But isn't that the basic fundamental purpose of Async? I wouldn't call it a "very specific scenario". As I understand it, the purpose of Async is to achieve asynchrony without extra threads, by breaking up a piece of work into manageable chunks (some of which may use threads, and be awaited; others being performed synchronously) and time-sharing them - that is, posting them piecemeal - on a single thread, usually the GUI thread.

    Given the above, my scenario is extremely common and run-of-the-mill.

    But from your post I can see you have no interest in discussing this further, and would rather I just shut up about it. Not understanding the "Awaiter facade" sufficiently to write my own solution, I have no alternative but to abandon Async until such time as it is fixed.

    And yes, I do mean "fixed". It is broken, because it doesn't do what it says on the tin, and the fact that you disagree does not change this.

  • Friday, February 10, 2012 6:45 PM
    Moderator
     
     Answered

    I'm sorry you feel I'm being disagreeable; that is not at all my goal.  I've been trying to help.  And in fact while you were writing your response, I was whipping up a simple example of something you might try on the custom awaiter approach.  I don't know if this will meet your needs, but you can give it a shot and let us know how it goes for you:

    public struct IdleAwaiter : INotifyCompletion
    {
        private static Queue<Action> m_actions = new Queue<Action>();

        static IdleAwaiter()
        {
            Application.Idle += delegate
            {
                if (m_actions.Count > 0)
                {
                    SynchronizationContext.Current.Post(s => ((Action)s)(), m_actions.Dequeue());
                }
            };
        }

        public IdleAwaiter GetAwaiter() { return this; }
        public bool IsCompleted { get { return false; } }
        public void GetResult() { }
        public void OnCompleted(Action continuation)
        {
            m_actions.Enqueue(continuation);
        }
    }

    Now with that, you can try writing code like:

    private async void button1_Click(object sender, EventArgs e)
    {
        for(int i=0; i<1000000; i++)
        {
            await default(IdleAwaiter);
            button1.Text = i.ToString();
        }
    }

    I hope that helps.



  • Friday, February 10, 2012 6:47 PM
     
     

    I'm sorry you feel I'm being disagreeable; that is not at all my goal.  I've been trying to help.  And in fact while you were writing your response, I was whipping up a simple example of something you might try on the custom awaiter approach.  I don't know if this will meet your needs, but you can give it a shot and let us know how it goes for you

    Perhaps I misinterpreted the tone of your previous post. I apologise, and thank you for the example. I will give it a go and see what I can do with it, and report back here.

    Cheers,
       Stuart.

  • Friday, February 10, 2012 7:08 PM
     
      Has Code

    Success! Your example solved the problem - thank you! - and I have made a few modifications (for performance and thread-safety), which I will show here.
    (The code is in VB, I'm afraid; but if I can translate C# into VB, I'm sure you can do the reverse.)

    <HideModuleName()>
    Public Module IdleAwaiterModule
       Public ReadOnly IdleAwaiter As New IdleAwaiterType
    End Module
    
    Public Class IdleAwaiterType
       Private Shared QueuedActions As New Queue(Of Action)()
    
       Shared Sub New()
          AddHandler Application.Idle, Sub()
             If Not QueuedActions.Any Then Return
             Dim NextAction As Action
             SyncLock QueuedActions
                If Not QueuedActions.Any Then Return
                NextAction = QueuedActions.Dequeue
             End SyncLock
             SynchronizationContext.Current.Post(Sub() NextAction(), Nothing)
          End Sub
       End Sub
    
       Public Function GetAwaiter() As IdleAwaiterType
          Return Me
       End Function
       Public ReadOnly Property IsCompleted As Boolean
          Get
             Return False
          End Get
       End Property
       Public Sub GetResult()
       End Sub
       Public Sub OnCompleted(continuation As Action)
          QueuedActions.Enqueue(continuation)
       End Sub
    End Class

    Then we can simply "Await IdleAwaiter".

    Hopefully this code will make its way into the Async framework, but in the mean time, thank you for your help.

    I'm still not sure why TaskEx.Await() puts the continuation ahead of windows events, but I will look at the BeginInvoke() link you supplied and see if it makes sense.

    Cheers,
       Stuart.

  • Friday, February 10, 2012 7:38 PM
     
      Has Code

    Hmm...unfortunately, attaching processing to the Idle event causes deadlocks, because the event is only raised if other events have occurred since the last time it was raised. In other words -

    Await IdleAwaiter
    Await IdleAwaiter
    - will cause deadlocks.

    Not to mention, of course, that doing it this way will mean the processing is done at a lower priority than all other events; instead of being on an equal footing.

    I will attempt to modify the code to use a custom Windows message and the Application.PreFilterMessage function.

  • Friday, February 10, 2012 7:40 PM
     
     
    I mean Application.AddMessageFilter(). Of course.
  • Friday, February 10, 2012 7:59 PM
     
     

    Nope, that's not working either. Oddly, when I post custom Windows messages to the message queue with the PostMessage API, I'm getting precisely the same problem I was getting with TaskEx.Await. I don't understand it.

    Surely if I post a custom Windows message to the message queue, the message pump should process earlier messages first? But it doesn't.

    I'm using RegisterWindowMessage() to get a message ID, then using PostMessage() in my OnCompleted() function, and using Application.AddMessageFilter() to register a PreFilterMessage() event handler, where I determine if my message has been received, and if so, I just invoke the queued callback.

    But it's not working.

  • Friday, February 10, 2012 8:12 PM
     
     

    Also, it seems that my deadlock when using the IdleAwaiter was due to wrapping my async code in a Treeview.BeginUpdate/EndUpdate block by means of a Using statement. That is, the problem goes away when I remove the calls to BeginUpdate/EndUpdate.

    But I don't know why.