Action block with async delegate stalling when launched from GUI thread [retitled]

Discussion Action block with async delegate stalling when launched from GUI thread [retitled]

  • 2011年8月5日 14:34
     
     

    This is not causing me any problems, but it seems to be a bug, so I thought I would pass it on.

    The following code runs as expected if I call it from a console application. If I call it from a button click handler in a windows forms application, however, the ActionBlock delegate executes twice with the first message, after which the system hangs with no further processing. This problem does not occur if I remove the "async" from the ActionBlock delegate and replace the await TaskEx.Delay() call by a Thread.Sleep() call.

            public static void ErrorTester()
            {
                TraceListener traceOut = Debug.Listeners[0];
                
                int maxMessages = 6;
                int processedMessages = 0;
                int baseTicks = Environment.TickCount;

                ExecutionDataflowBlockOptions abOpts =
                    new ExecutionDataflowBlockOptions()
                {
                    MaxDegreeOfParallelism = 1,
                    TaskScheduler = TaskScheduler.Default
                }
                ActionBlock<string> ab1 = new ActionBlock<string>(
                    async (msg) =>
                    {
                        double ts = 0.001 * (Environment.TickCount - baseTicks);
                        string lineOut = msg + ", " + ts.ToString();
                        traceOut.WriteLine(lineOut);
                        Console.WriteLine(lineOut);
                        await TaskEx.Delay(500);
                        Interlocked.Increment(ref processedMessages);
                    }, abOpts);
                
                DataflowBlockOptions bbOpts = new DataflowBlockOptions()
                {
                    TaskScheduler = TaskScheduler.Default
                }
                BufferBlock<string> bbA = new BufferBlock<string>(bbOpts);
                bbA.LinkTo(ab1, false);

                for (int i1 = 0; i1 < maxMessages; ++i1) {
                    bbA.Post("A" + i1);
                }
                while (processedMessages < maxMessages) {
                    Thread.Sleep(500);
                }
                bbA.Complete();
                ab1.Complete();
            }

     


    Chris Stover
    • 已编辑 Chris Stover 2011年8月8日 13:05 New title will better describe helpful content in answers
    •  

全部回复

  • 2011年8月5日 20:33
     
     

    Hi Chris,

     

    Your example brings up something that we need to call out: when the asynchronous versions of ActionBlock (or TransformBlock or TransformManyBlock) are constructed, the TaskScheduler option becomes irrelevant. That is because the TPL Dataflow runtime does not schedule the processing tasks in that case.

     

    When the app is running within a console, the Current task scheduler is the Default task scheduler which is the ThreadPool, i.e. you have the thread on which your method is executed plus a bunch of additional threads where tasks get scheduled.

     

    When your app is running in a GUI context, the Current task scheduler is the WinForms scheduler which has one thread total including the thread where your method is executed. Why am I mentioning the thread where your method is executed? Because this code, at the end of the method, is incorrect:

                 while (processedMessages < maxMessages) {

                      Thread.Sleep(500);

                 }

    It keeps spinning without allowing other threads to get scheduled. Again, the “current” thread here is the “only” thread.

     

    The correct way to finish the scenario is this:

                 bbA.Complete();

                 bbA.Completion.ContinueWith(completion => ab1.Complete());

                 ab1.Completion.Wait();

     

     

    Zlatko Michailov

    Software Development Engineer, Parallel Computing Platform

    Microsoft Corp.


    This posting is provided "AS IS" with no warranties, and confers no rights.
  • 2011年8月6日 4:46
    所有者:
     
     

    Some clarifications on Zlatko's response...

    First, Zlatko is correct that part of the problem here is that you're blocking the UI thread.  You've created a deadlock by blocking the UI threads until all messages are processed, and for reasons I'm about to explain, the blocks won't process these messages until the UI thread is available to process some work.

    Second, you're not actually processing the first message twice... you're just outputting both trace.WriteLine and Console.WriteLine, so you're seeing two outputs for the same message processing (if you set a breakpoint inside of the delegate passed to the ActionBlock, you'll see that it only gets hit once.

    Third, the scheduling isn't quite as Zlatko describes.  TaskScheduler.Default will always go to the ThreadPool (even if you're running on the UI thread), and thus the tasks scheduled by the dataflow blocks in your example will also be scheduled to the UI thread.  However, now things get a bit tricky.  When awaiting a task, as you're doing in your async delegate, the await functionality checks whether there's a current SynchronizationContext before checking the current TaskScheduler.  On top of this, when you create a dataflow block that runs user-provided delegates, like ActionBlock does, it captures the current ExecutionContext.  Unfortunately, the public API ExecutionContext.Capture() also captures and restores the SynchronizationContext.Current that was on the thread where the block was created.  This means that when we start running the delegate, we're doing so on a ThreadPool thread that has the UI's SynchronizationContext set as current, which means that even though we're on a ThreadPool thread, the await inside of the async delegate will see the UI SynchronizationContext and post the continuation back to the UI.  The UI is blocked waiting for all of the messages to be processed, so the processing of the first message can't complete, so none of the messages complete, so the UI remains blocked... deadlock.

    Any easy way to see that this is the problem is by adding a call to ExecutionContext.SuppressFlow() prior to instantiating the ActionBlock (you can RestoreFlow after creating the block).  That will prevent the ActionBlock from capturing and restoring the ExecutionContext, and thus restoring the UI's SynchronizationContext, and as such await won't find a SynchronizationContext to target and will remain.  Another quick fix would be to tag your TaskEx.Delay(500) with ".ConfigureAwait(false)", as that will prevent the await from looking for a current SynchronizationContext or TaskScheduler and will cause it to just continue execution on the ThreadPool.

    For .NET vNext, this problem won't manifest.  For our .NET 4 builds, we're considering other workarounds for future releases.

    I hope that helps.

  • 2011年8月8日 13:08
     
     

    Thanks for the very informative replies.

    >> ...you're not actually processing the first message twice... you're just outputting both trace.WriteLine and Console.WriteLine, so you're seeing two outputs for the same message processing...

    - Apologize for not catching something so obvious.


    Chris Stover