0

I have the code below and it just hangs on the Task.WaitAll line. I'm guessing this is due a deadlock where the main thread is waiting on it's synch context to continue execution but this context is also required by one of the continuations in order to finish it's execution as outlined here:

http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

 protected override async Task<Person> GetOrders()
 {
     var person = new Person();
     var task1 = GetPersonOrders(person);
     var task2 = GetPersonAddresses(person);
     var tasks = new List<Task>(){task1, task2}
     Task.WaitAll(tasks.ToArray());
 }

public async Task GetPersonOrders(Person person)
{
   ...
   person.PersonOrders = await GetPersonOrdersFromRepository();
}

public async Task<List<Order>> GetPersonOrdersFromRepository()
{
   ...
   return await CallSomeWebService;
}

public async Task GetPersonAddresses(Person person)
{
   ...
   person.Addresses = await GetPersonAddressesFromRepository(); 
}

public async Task<List<Address>> GetPersonAddressesFromRepository()
{
   ...
   return await CallSomeWebService;
}

Questions

1) Why does altering the final of GetOrders() to

await Task.WhenAll(tasks);

fix everything? Why is the deadlock resolved by awaiting Task.WhenAll over Task.WaitAll?

2) Are these tasks still being run in Parallel with the Task.WhenAll modification?

user1054637
  • 695
  • 11
  • 28
  • `await` does cooperative multitasking, not parallel processing. `await WhenAll` cooperates with the caller, `WaitAll` not only doesn't cooperate with the caller, it doesn't cooperate with the subtasks. – Ben Voigt Mar 16 '18 at 22:36
  • Actually...Does `WaitAll()` require the tasks to have been started manually? Maybe that's the difference here? – zzxyz Mar 16 '18 at 22:39
  • @BenVoigt - I removed my comment, because yeah, I think it was incorrect. I'm a little unsure what you mean by "multitasking is not parallel processing". In general, true. But if one launches two truly async tasks sequentially, I've found that given available threads, they process in parallel (each having its own thread). Am I wrong in this example? – zzxyz Mar 16 '18 at 22:45
  • 2
    @zzxyz: It depends on the environment you're running in (specifically, the `TaskScheduler.Current` implementation.) Even if `GetPersonAddressesFromRepository` and `CallSomeWebService` used other threads (they probably don't, because I/O is inherently asynchronous), the moment you `await` them some `TaskScheduler`s will try to execute the rest of your method on the same thread. This happens in environments like WPF, Windows Forms, and modern versions of ASP.NET. – StriplingWarrior Mar 16 '18 at 23:03

1 Answers1

4

Why does altering the final of GetOrders() to await Task.WhenAll(tasks); fix everything? Why is the deadlock resolved by awaiting Task.WhenAll over Task.WaitAll?

Because as soon as you hit await, the GetOrders() method returns a Task, indicating that it's done all that it can do synchronously and other tasks can take over this thread.

When the tasks you've scheduled get responses back from the web server, repository, etc., the task scheduler is able to assign a thread for the Tasks' continuations to complete on.

If you were running this in an environment where those tasks could be completed in parallel synchronization contexts, then you wouldn't be getting deadlocked even with Task.WaitAll().

Likewise, if you consistently used ConfigureAwait(false) with every await in the call stack then you wouldn't have this problem either, as that would inform the task scheduler that you don't care whether the resulting task continues on the same synchronization context it started on.

Are these tasks still being run in Parallel with the Task.WhenAll modification?

No. They are running Concurrently, but not in Parallel.

Remember that this problem originated with the Tasks not being able to continue execution until the thread that they started on gets released. The fact that they're only allowed to run on one thread at a time means they're not going to run in parallel.

See What is the difference between asynchronous programming and multithreading? for more details.

StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • "Remember that this problem originated with the Tasks not being able to continue execution on the same thread they started on." The same synch context you mean right? ..... How would I run these async tasks in parallel...using Parallel.ForEach? – user1054637 Mar 16 '18 at 23:06
  • I'm a little confused. What would be causing these tasks to be bound to a single thread? I believe I understand the distinction, but I'm failing to understand why it applies to this example. I'll typically use a `UIElement.Dispatcher.Invoke()` to update the UI from a worker task, and I just feel like I've never run into this being anything other than semantics in C#. But clearly it can be. – zzxyz Mar 16 '18 at 23:06
  • Ok, you basically answered this question above. Thank you. – zzxyz Mar 16 '18 at 23:09
  • 1
    It says a lot about how well-designed these classes these are that I've used them so extensively without problems at my apparently somewhat low level of knowledge about how they work. – zzxyz Mar 16 '18 at 23:13
  • @StriplingWarrior "as that would inform the task scheduler that you don't care whether the resulting task continues on the same thread it started on." ... when I check the threadId in GetOrders() prior to any other code being executed and then do the same check in any of the continuations...while the synch context is the same (AspNetSynchContext) the threadIds are different. Shouldn't it be the same threadId?...this is *without* amending the code to reflect ConfigureAwait(false) down the call chain. – user1054637 Mar 18 '18 at 11:00
  • @user1054637: Good catch. I was incorrect to say "the same thread": in a technology like Windows Forms, the synchronization context does continue on the same thread (the "UI thread"), but ASP.NET doesn't care so much whether the continuation happens on the same *thread* as it cares that only one thread is running continuations for a given request. Perhaps the thread your request began on got assigned to another request: since there's no "UI thread" it doesn't matter *which* thread is used for the request's continuation, so much as that there's only one running at a time. – StriplingWarrior Mar 18 '18 at 22:57
  • However, the point remains that the original thread can't be released until the tasks are done, and the tasks can't be continued until the original thread is released. – StriplingWarrior Mar 18 '18 at 22:58
  • I updated my wording. Let me know if that's any clearer, or if it still needs work. – StriplingWarrior Mar 18 '18 at 23:05