none
Dispatcher arbitrarily aborts operations RRS feed

  • Question

  • I found a curious problem with custom Dispatcher message loops in a background thread.  Queued operations are sometimes aborted, apparently for no reason.  Here's the test program that demonstrates the issue (compile with /r:WindowsBase.dll):

    using System;  
    using System.Threading;  
    using System.Windows.Threading;  
     
    public static class DispatcherTest {  
     
        public static void Main() {  
     
            var thread = new Thread(new ThreadStart(delegate {  
                Console.WriteLine("BGND: Waiting for work items...");  
                Dispatcher.Run();  
                Console.WriteLine("BGND: Exiting background thread.");  
            }));  
            thread.IsBackground = true;  
            thread.Start();  
     
            Console.WriteLine("MAIN: Waiting for dispatcher...");  
            Dispatcher dispatcher = Dispatcher.FromThread(thread);  
            while (dispatcher == null) {  
                if (!thread.IsAlive)  
                    throw new InvalidOperationException();  
     
                Thread.Sleep(1);  
                dispatcher = Dispatcher.FromThread(thread);  
            }  
     
            Console.WriteLine("MAIN: Queuing background work items...");  
            var op1 = dispatcher.BeginInvoke(new Action(delegate {  
                Console.WriteLine("BGND: Hello One from background thread!");  
            }));  
     
            var op2 = dispatcher.BeginInvoke(new Action(delegate {  
                Console.WriteLine("BGND: Hello Two from background thread!");  
            }));  
     
            op1.Wait(); op2.Wait();  
            Console.WriteLine("MAIN: Hello One = {0}, Hello Two = {1}", op1.Status, op2.Status);  
     
            dispatcher.InvokeShutdown();  
            Console.WriteLine("MAIN: Exiting main thread.");  
        }  

    Most of the time, both Actions are enqueued and executed correctly, and I get the following output:

    MAIN: Waiting for dispatcher...
    BGND: Waiting for work items...
    MAIN: Queuing background work items...
    BGND: Hello One from background thread!
    BGND: Hello Two from background thread!
    MAIN: Hello One = Completed, Hello Two = Completed
    MAIN: Exiting main thread.
    BGND: Exiting background thread.

    Sometimes, however -- as frequently as maybe one time out of ten -- the two queued operations are prematurely aborted, and I get the following output instead:

    MAIN: Waiting for dispatcher...
    BGND: Waiting for work items...
    MAIN: Queuing background work items...
    MAIN: Hello One = Aborted, Hello Two = Aborted
    MAIN: Exiting main thread.

    What on earth is going on here?  The thread and its Dispatcher are obviously available since I can call BeginInvoke, and the initial "Waiting for work items" message appears.  I checked thread.IsAlive and dispatcher.HasShutdownStarted/Finished after the operations report Aborted, but the former is always true and the two latter are always false.  So it seems that the background thread is in fact still alive and well at this point, and probably even running its Dispatcher loop.  But why then are those operations aborted?

    Thursday, January 29, 2009 7:51 AM

Answers

  • Got it, there was as race condition between the point in time when Dispatcher.FromThread would return a valid Dispatcher object, and the point in time when that Dispatcher had fully initialized its message loop on the background thread.  This fix needs to be applied to the first BeginInvoke call:

            DispatcherOperation op1;  
            do {  
                op1 = dispatcher.BeginInvoke(new Action(delegate {  
                    Console.WriteLine("BGND: Hello One from background thread!");  
                }));  
            } while (op1.Status == DispatcherOperationStatus.Aborted);  
     

    The MSDN Library documentation for Dispatcher.BeginInvoke says that: "If BeginInvoke is called on a Dispatcher that has shut down, the status property of the returned DispatcherOperation is set to Aborted."

    This appears to be what has happened here -- except that the Dispatcher was in fact not shut down (and HasShutdownStarted/Finished were false) but rather had not fully started up yet!  There is no flag to test for this condition, and I would have expected a newly created Dispatcher to accept and store incoming BeginInvokes until the message loop is ready to process them.  Instead, they are rather rudely rejected with an "Aborted" status.  Well, that's good to know...

    • Marked as answer by Christoph Nahr Thursday, January 29, 2009 9:58 AM
    Thursday, January 29, 2009 9:57 AM

All replies

  • Got it, there was as race condition between the point in time when Dispatcher.FromThread would return a valid Dispatcher object, and the point in time when that Dispatcher had fully initialized its message loop on the background thread.  This fix needs to be applied to the first BeginInvoke call:

            DispatcherOperation op1;  
            do {  
                op1 = dispatcher.BeginInvoke(new Action(delegate {  
                    Console.WriteLine("BGND: Hello One from background thread!");  
                }));  
            } while (op1.Status == DispatcherOperationStatus.Aborted);  
     

    The MSDN Library documentation for Dispatcher.BeginInvoke says that: "If BeginInvoke is called on a Dispatcher that has shut down, the status property of the returned DispatcherOperation is set to Aborted."

    This appears to be what has happened here -- except that the Dispatcher was in fact not shut down (and HasShutdownStarted/Finished were false) but rather had not fully started up yet!  There is no flag to test for this condition, and I would have expected a newly created Dispatcher to accept and store incoming BeginInvokes until the message loop is ready to process them.  Instead, they are rather rudely rejected with an "Aborted" status.  Well, that's good to know...

    • Marked as answer by Christoph Nahr Thursday, January 29, 2009 9:58 AM
    Thursday, January 29, 2009 9:57 AM
  • Turns out that Dispatcher.Invoke is subject to the same race condition, and worse: you can't even discover that an Invoke call has been ignored in this case!  I've added notes regarding my findings to the two main MSDN Library entries for Invoke & BeginInvoke since they did not mention this race condition:

    http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.invoke.aspx
    http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.begininvoke.aspx

    Corrections are welcome if I've misunderstood anything about the behavior of Dispatcher or these methods.

    Thursday, January 29, 2009 10:27 AM
  • You ought to post this in the WPF forum.  However, I can make an educated guess.  You are using this class in the wrong execution environment.  Windows Forms has very similar plumbing, utilized by Control.Invoke().  This can only work properly if a SynchronizationContext is available, WF's provider is the WindowsFormsSynchronizationContext class.  WPF surely has something similar.

    The default SC doesn't synchronize anything, invocation requests are dispatched on a threadpool thread.  The only SCs I've ever seen are ones that use the Windows message loop.  COM uses it too.  It is the only safe place in a Windows program where you can inject a method call into a thread and minimize the odds of re-entrancy problems.

    Console mode programs don't have such a safe place.  With the default SC dispatching requests on threads without synchronization, you should expect to run into classic concurrency problems like race conditions.  To verify my guesses, display the CurrentThread.ManagedThreadId property in your anonymous delegates and compare them to the main thread's ID.

    Hans Passant.
    Thursday, January 29, 2009 1:05 PM
    Moderator
  • Yeah, the Dispatcher is probably maintained by the WPF team -- I wasn't thinking about that since I wasn't trying to use it in a WPF environment.

    As for your guess, your assumptions are only partly correct.  Dispatcher always uses a SynchronizationContext-derived class internally, called DispatcherSynchronizationContext.  As far as I can tell Dispatcher never attempts to use the default SC or the ThreadPool.  You are correct that Dispatcher operation is based on a message loop using Win32 GetMessage etc., but that loop is actually implemented by Dispatcher itself (in managed code), in the private method PushFrameImpl that gets called by Dispatcher.Run.

    I was not entirely sure myself if Dispatcher would work in a console-mode program, but it does appear to work fine, aside from this accidental race issue.  Note that I don't try to dispatch anything to the console program's main thread, just to a second thread that's running a Dispatcher.Run loop.  Moreover, I have verified that the same race issue I encountered in the console program also appears when the test case is run within a fully fledged WPF application.  So if Dispatcher has any assumptions or requirements about running within a WPF application, I haven't found them yet!

    Thursday, January 29, 2009 2:11 PM
  • Hello, Chris -

    I've taken a look at the Dispatcher internals, and I believe that you're correct.

    More specifically, Dispatcher.Dispatcher will add itself to the list of dispatchers before it creates its message window. This list of dispatchers is what Dispatcher.FromThread reads, so it is possible that the child thread is still executing the constructor for the dispatcher when the main thread gets a reference to it.

    When creating child threads with dispatchers, I always have them call Dispatcher.CurrentDispatcher.VerifyAccess() and then notifying the main thread that they are ready (via BeginInvoke or MRE) before entering Dispatcher.Run(). This is actually a reflex dating back to the days when I discovered that CreateThread could return before the APC queue had been set up.

    You might be interested in a class I wrote called ActionDispatcher, which maintains a queue of actions to run. It's much simpler than Dispatcher and uses MREs for synchronization instead of Windows messages. This class (along with its companion ActionDispatcherSynchronizationContext) is slated for release this Saturday on Nito.Async. It's designed with console apps and Windows services in mind.

           -Steve

    Thursday, January 29, 2009 6:23 PM
  • Thanks for your comments. Steve.  Interesting that the Dispatcher object can get published before it's even fully initialized -- sounds like a bug to me, frankly.

    Explicit synchronization is always an option, of course, but I had rather hoped to avoid that.  I'll be sure to check out the Nito.Async project when your ActionDispatcher class is published, perhaps that's a better solution after all.

    Friday, January 30, 2009 8:34 AM
  • I thought about referring to this as a "bug", but I'm not quite sure that would be accurate.

    The only problem with the way it's working now is that you're using Dispatcher.FromThread as a synchronizing object. In my mind, it just wasn't meant for that. On the other hand, I can certainly see how the results of your code are surprising - not really expected behavior, either.

    This would definitely be interesting to bring up to the WPF team and see what they think.

           -Steve

    Saturday, January 31, 2009 12:27 AM
  • Chris -

    The ActionDispatcher/ActionDispatcherSynchronizationContext have (finally) been released. It was later than I was hoping for, but I had a fair amount of documentation cleanup to do before officially releasing (Nito.Async 1.1 includes chm docs for the first time).

    My ActionDispatcher does have a few conceptual differences from the WPF Dispatcher; these are covered in the documentation.

           -Steve
    Thursday, February 26, 2009 4:56 PM
  • Chris,

    You're correct in that there is a race condition; however, there is another way that is cleaner and doesn't involve the 'while' loop: 

    using System;
    using System.Threading;
    using System.Windows.Threading;
    
    public static class DispatcherTest
    {
        public static void Main()
        {
            AutoResetEvent evt = new AutoResetEvent(false);
            Dispatcher dispatcher = null;
    
            var thread = new Thread(new ThreadStart(delegate
            {
                Console.WriteLine("BGND: Waiting for work items...");
                dispatcher = Dispatcher.CurrentDispatcher;
                evt.Set();
                Dispatcher.Run();            
                Console.WriteLine("BGND: Exiting background thread.");
            }));
            thread.IsBackground = false;
            thread.Start();
    
            evt.WaitOne();
    
            Console.WriteLine("MAIN: Queuing background work items...");
            var op1 = dispatcher.BeginInvoke(new Action(delegate
            {
                Console.WriteLine("BGND: Hello One from background thread!");
            }));
    
            var op2 = dispatcher.BeginInvoke(new Action(delegate
            {
                Console.WriteLine("BGND: Hello Two from background thread!");
            }));
    
            op1.Wait(); op2.Wait();
            Console.WriteLine("MAIN: Hello One = {0}, Hello Two = {1}", op1.Status, op2.Status);
    
            dispatcher.InvokeShutdown();
            Console.WriteLine("MAIN: Exiting main thread.");
        }
    }

    There are a few things to note:

    1.  The AutoResetEvent object synchronizes your thread's access to the dispatcher object, so you can guarantee the object has been gathered before you try to do anything with it.
    2.  Using dispatcher = Dispatcher.CurrentDispatcher; will create the dispatcher for your current thread and return a valid object.
    3.  Setting the thread's IsBackground property to false will ensure the thread does not prematurely terminate when the application exits.  You'll notice in your version sometimes the final message in the thread would be cut off.

    Thanks,
    -Doug

    Tuesday, June 16, 2009 2:06 PM
  • I love you Chris!

    I was struggling getting my Dispatcher to work properly in my console application but your code fixed all the issues I had!

    Thanks!

    Friday, November 26, 2010 10:40 PM