Henrik
Henrik

Reputation: 11

How to block a method while an asyncronous method is running without causing deadlocks in the UI thread

I have several async methods that need to synchronize back to the main ui thread.

async Task MyAsyncMethod() {
    await DoSomeThingAsync().ConfigureAwait(true);
    DoSomethingInTheGui();
}

now i need to call them from a syncronous event handler that is triggered from the gui thread, and the event handler cannot complete until the async method is done. so MyAsyncMethod().Wait() is not an option, neither is some kind of fire-and-forget solution.

This approach using Nito.AsyncEx seemed promising, but it still deadlocks: https://stackoverflow.com/a/9343733/249456

The only solution i've found seems like a hack to me:

public void RunInGui(Func<Task> action)
{
    var window = new Window();
    window.Loaded += (sender, args) => action()
       .ContinueWith(p => {
            window.Dispatcher.Invoke(() =>
            {
                window.Close();
            });
        });
        window.ShowDialog(); 
}

Is there a way to get the same effect (block the calling method, allow syncronization back to the gui thread) without creating a new window?

Note: I am aware that refactoring would probably be the best option, but that is a massive undertaking we have to do over longer time. Also worth mentioning that this is a plugin for Autodesk Inventor. The api has several quirks, and all API calls (even non-ui related) have to be executed from the main/ui thread.

Also worth mentioning is that we keep a reference to the main threads dispatcher and use MainThreadDispatcher.InvokeAsync(() => ... ) all around the codebase.

Upvotes: 1

Views: 100

Answers (1)

Stephen Cleary
Stephen Cleary

Reputation: 456507

The only solution i've found seems like a hack to me

All sync-over-async solutions are hacks. I enumerate the most common ones in my article on brownfield async. It's important to note that no solution exists for arbitrary code. Every solution either deadlocks or throws in some situation.

One of the easier hacks is to block on a thread pool thread:

Task.Run(() => action()).Wait();

However, if your action() requires a UI context, then that won't work.

The next step is to attempt using a single-threaded context. This is the approach taken by my AsyncContext type:

AsyncContext.Run(() => action());

This would deadlock if action is waiting for the UI thread's message queue to be pumped. Since this deadlocks but Window works, this appears to be the case.

Dropping further down a level, you could use nested pumping. Please note that nested pumping opens up a whole world of hurt due to unexpected reentrancy. There's some code on GitHub that shows how to do nested pumping on WPF. However, I highly recommend that you refactor your code rather than embrace reentrancy.

Upvotes: 8

Related Questions