February 23rd, 2024

Gotcha: Be careful how you shut down your dispatcher queues

Last time, we learned that it is important to shut down your dispatcher queues. This is done by calling Dispatcher­Queue­Controller.Shutdown­Queue­Async(). The operation completes after the queue has shut down and its thread has become useless. But there’s a catch.

The catch is the case where you await a call to Dispatcher­Queue­Controller.Shutdown­Queue­Async() from the dispatcher queue thread. In this case, you are asking the current thread to shut itself down, and then expecting to regain control on the very thread that has been shut down.

You are now skating on thin ice.

namespace winrt
{
    using namespace winrt::Windows::Foundation;
    using namespace winrt::Windows::System;
}

winrt::IAsyncAction Example()
{
    // Create a dispatcher queue controller
    // for a new dispatcher queue thread
    auto controller =
        winrt::DispatcherQueueController::CreateOnDedicatedThread();

    // To demonstrate the problem, switch to the dispatcher queue thread
    co_await winrt::resume_foreground(controller.DispatcherQueue());

    // Shut down the dispatcher queue while we're on its thread (!)
    co_await controller.ShutdownQueueAsync();
}

Both C# and C++/WinRT have a default policy of resuming execution after an await / co_await in the same COM apartment in which the await started.¹ If you start the await on the dispatcher queue thread, then execution resumes on a dispatcher queue thread that has already shut down and is about to exit.

Even though the thread is about to exit, it is still a single-threaded COM apartment (at least for a little while longer), so any await / co_await / PPL task is going to try to return to that single-thread COM apartment which has been destroyed. Furthermore, any operations you perform that involve multiple threads may fail because they no longer have a way to get back to the dispatcher queue thread.

winrt::IAsyncAction Example()
{
    // Create a dispatcher queue controller
    // for a new dispatcher queue thread
    auto controller =
        winrt::DispatcherQueueController::CreateOnDedicatedThread();

    // To demonstrate the problem, switch to the dispatcher queue thread
    co_await winrt::resume_foreground(controller.DispatcherQueue());

    // Shut down the dispatcher queue while we're on its thread (!)
    co_await controller.ShutdownQueueAsync();

    // Oh no, this will try to resume on the dispatcher queue thread
    // after it has been shut down.                                 
    co_await winrt::Launcher::LaunchUriAsync(                       
        winrt::Uri{L"https://microsoft.com"});                      
                                                                    
    DoSomeMoreStuff();                                              
}

That call to Launch­Uri­Async is in big trouble. After the launch completes, it’s going to try to return to the dispatcher queue thread, which likely no longer exists by the time it completes. The code will throw an exception representing RPC_E_DISCONNECTED, and nothing after the co_await will execute.

Probably the best way to avoid this problem is to shut down the dispatcher queue from a background thread. That way, you never find yourself running on a thread that is slated for death.

winrt::IAsyncAction Example()
{
    // Create a dispatcher queue controller
    // for a new dispatcher queue thread
    auto controller =
        winrt::DispatcherQueueController::CreateOnDedicatedThread();

    // Switch to the dispatcher queue thread
    co_await winrt::resume_foreground(controller.DispatcherQueue());

    // Get off the dispatcher queue thread                        
    // before shutting it down.                                   
    co_await winrt::resume_background();                          
                                                                  
    // Shut down the dispatcher queue thread from a safe distance.
    co_await controller.ShutdownQueueAsync();

    // This code is now safely running on a background thread.
    co_await winrt::Launcher::LaunchUriAsync(
        winrt::Uri{L"https://microsoft.com"});

    DoSomeMoreStuff();
}

¹ PPL also has the default policy of resuming execution in the same COM apartment provided the task chain started with a Windows Runtime IAsync­Xxx.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

4 comments

Discussion is closed. Login to edit/delete existing comments.

  • Joe Beans · Edited

    I learned that in order to keep my limited-lifespan UI thread dispatchers running long enough, I have to ref-count them.

    For each such thread in C# I have a [ThreadStatic] Task that's connected to a countdown, it "signals" when the count is zero. I have a static helper method that returns an opaque IDisposable. When it's called, it increments the count, and when the handle is disposed, it decrements the count. When I decide to terminate...

    Read more
  • Neil Rashbrook

    Wouldn’t it be easier to make the call fail to shut down the current thread? I would have thought that the resulting exception would make it blindingly obvious that you’re trying to do something that’s not going to work.