0

I recently come to a deadlock issue similar at the one described here: An async/await example that causes a deadlock

This question is not a duplicate of the one below.

Because I have access to the async part of the code I have been able to use the solution described by @stephen-cleary in his blog post here: http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

So I added ConfigureAwait(false) on the first awaited Task.

I can't use the second solution (put async everywhere) because of the amount of impacted code.

But I keep asking myself, what if I can't modify the called async code (ex: extern API).

So I came to a solution to avoid the dead lock. A way to Join the task. By Join I mean a way to wait for the targeted task to end without blocking other tasks that's run on the current thread.

Here is the code:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Call back the dispatcher to allow other Tasks on current thread to run
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

I want to emphasize that I'm not sure that the correct name for this method is Join.

My question is why Task.Wait() is not implemented this way or may be optionally used this way ?

Orace
  • 7,822
  • 30
  • 45
  • 1
    Tasks are implemented in a forward propagating manner. When one tasks complete, it informs *something* and new tasks that are waiting is kicked off from that. Additionally, waiting for a task to complete using `.Wait()` is documented to block the thread until the task completes, your implementation is basically "unblock the thread". Why then didn't you just schedule your code that should run after the task completes as a new task, continued from the first one? Basically, there *are* good mechanisms in the framework for doing exactly what you want. – Lasse V. Karlsen Sep 26 '18 at 10:25
  • Also, be aware that "why did *they* implement *this* in *this particular way*?" is a question that very often is closed as "Primarily based on opinion" since very seldom do we have the luxury of getting the wisdom of the original designers and usually have to just guess and come up with good and *plausible* reasons for why it is this way. – Lasse V. Karlsen Sep 26 '18 at 10:26
  • The right answer, despite your apparent distaste for it, is the "add `async` everywhere". It's often observed that when you adopt this feature, it tends to virally impact the whole call stack. – Damien_The_Unbeliever Sep 26 '18 at 10:30
  • And while your code may work in a web application, you can deadlock a winforms application in the same manner with the code from your original question, in which case the "unblock" code would have to pump Windows messages instead of executing tasks, so this is by no means a general solution and is thus one of those plausible reasons for why they didn't just implement .Wait as a polling construct. It depends on your scenario *how* you have to do the polling. – Lasse V. Karlsen Sep 26 '18 at 10:30
  • Lastly, and after this I'll stop commenting, now in 2018 the languages and runtimes are focusing on creating good and *performant* solutions, and tying up a thread waiting for a task in a polling manner is not a good way to create performant and scaling solutions. Making it async everywhere *is*. – Lasse V. Karlsen Sep 26 '18 at 10:32

1 Answers1

3

This Join methods is simply busy waiting for the task to complete with an added equivalent of Application.DoEvents(). By repeatedly calling into the dispatcher like that you have essentially implemented a nested message pump that keeps the UI alive.

This is a really bad idea because it drives the CPU to 100%.

You really need to treat the UI message loop correctly and get off the UI thread when waiting. await is great for that.

You say that you really want to avoid making all code async aware because it would be a lot of work. Maybe you can smartly use the await Task.Run(() => OldSynchronousCode()); pattern to avoid a lot of the work to do that. Since this runs on the UI thread the frequency of such calls should be very low. This means that the overhead caused by this is also very low and it's not an issue.

usr
  • 168,620
  • 35
  • 240
  • 369