ChatGPT解决这个技术问题 Extra ChatGPT

await vs Task.Wait - Deadlock?

I don't quite understand the difference between Task.Wait and await.

I have something similar to the following functions in a ASP.NET WebAPI service:

public class TestController : ApiController
{
    public static async Task<string> Foo()
    {
        await Task.Delay(1).ConfigureAwait(false);
        return "";
    }

    public async static Task<string> Bar()
    {
        return await Foo();
    }

    public async static Task<string> Ros()
    {
        return await Bar();
    }

    // GET api/test
    public IEnumerable<string> Get()
    {
        Task.WaitAll(Enumerable.Range(0, 10).Select(x => Ros()).ToArray());

        return new string[] { "value1", "value2" }; // This will never execute
    }
}

Where Get will deadlock.

What could cause this? Why doesn't this cause a problem when I use a blocking wait rather than await Task.Delay?

@Servy: I will get back with a repo as soon as I have time. For now it works with Task.Delay(1).Wait() which is good enough.
Task.Delay(1).Wait() is basically the exact same thing as Thread.Sleep(1000). In actual production code it is rarely appropriate.
@ronag: Your WaitAll is causing the deadlock. See the link to my blog in my answer for more details. You should use await Task.WhenAll instead.
@ronag Because you have ConfigureAwait(false) a single call to Bar or Ros won't deadlock, but because you have an enumerable that is creating more than one and then waiting on all of those, the first bar will deadlock the second. If you await Task.WhenAll instead of waiting on all of the tasks, so that you don't block the ASP context, you'll see the method return normally.
@ronag Your other option would be to add the .ConfigureAwait(false) all the way up the tree until you block, that way nothing is ever trying to get back to the main context; that would work. Another option would be to spin up an inner synchronization context. Link. If you put the Task.WhenAll in an AsyncPump.Run it will effectively block on the whole thing without you needing to ConfigureAwait anywhere, but that's probably an overly-complex solution.

M
Mario S

Wait and await - while similar conceptually - are actually completely different.

Wait will synchronously block until the task completes. So the current thread is literally blocked waiting for the task to complete. As a general rule, you should use "async all the way down"; that is, don't block on async code. On my blog, I go into the details of how blocking in asynchronous code causes deadlock.

await will asynchronously wait until the task completes. This means the current method is "paused" (its state is captured) and the method returns an incomplete task to its caller. Later, when the await expression completes, the remainder of the method is scheduled as a continuation.

You also mentioned a "cooperative block", by which I assume you mean a task that you're Waiting on may execute on the waiting thread. There are situations where this can happen, but it's an optimization. There are many situations where it can't happen, like if the task is for another scheduler, or if it's already started or if it's a non-code task (such as in your code example: Wait cannot execute the Delay task inline because there's no code for it).

You may find my async / await intro helpful.


I think there is a missunderstanding, Wait works fine await deadlocks.
No, the task scheduler won't do that. Wait blocks the thread, and it cannot be used for other things.
@ronag My guess is you just got your method names mixed up and your deadlock was actually caused with the blocking code and worked with the await code. Either that, or the deadlock was unrelated to either and you mis-diagnosed the problem.
@hexterminator: This is by design - it works great for UI apps, but does tend to get in the way for ASP.NET apps. ASP.NET Core has fixed this by removing the SynchronizationContext, so blocking within an ASP.NET Core request no longer deadlocks.
@StephenCleary That's so trippy. I'm here because I'm having trouble understanding a bit of the Concurrency in C# Cookbook where you you first mention deadlocks, and after some time I realize that you're the author.
D
David Ferenczy Rogožan

Based on what I read from different sources:

An await expression does not block the thread on which it is executing. Instead, it causes the compiler to sign up the rest of the async method as a continuation on the awaited task. Control then returns to the caller of the async method. When the task completes, it invokes its continuation, and execution of the async method resumes where it left off.

To wait for a single task to complete, you can call its Task.Wait method. A call to the Wait method blocks the calling thread until the single class instance has completed execution. The parameterless Wait() method is used to wait unconditionally until a task completes. The task simulates work by calling the Thread.Sleep method to sleep for two seconds.

This article is also a good read.


"Isn't that technically incorrect then? Can someone please clarify?" - can I clarify; are you asking that as a question? (I just want to be clear whether you're asking vs answering). If you're asking: it may work better as a separate question; it is unlikely to gather new responses here as an answer
I have answered the question and asked a separate question for the doubt I had here stackoverflow.com/questions/53654006/… Thanks @MarcGravell. Can you please remove your deletion vote for the answer now?
"Can you please remove your deletion vote for the answer now?" - that isn't mine; thanks to the ♦, any such vote by me would have taken effect immediately. I don't, however, think that this answers the key points of the question, which is about the deadlock behaviour.
This is not true. Until first await not reached everything is blocked
@user1785960 though you are right, it doesn't mean the fact the answer is useless.
u
user1785960

Some important facts were not given in other answers:

"async await" is more complex at CIL level and thus costs memory and CPU time.

Any task can be canceled if the waiting time is unacceptable.

In the case "async await" we do not have a handler for such a task to cancel it or monitoring it.

Using Task is more flexible then "async await".

Any sync functionality can by wrapped by async.

public async Task<ActionResult> DoAsync(long id) 
{ 
    return await Task.Run(() => { return DoSync(id); } ); 
} 

"async await" generate many problems. We do not now is await statement will be reached without runtime and context debugging. If first await not reached everything is blocked. Some times even await seems to be reached still everything is blocked:

https://github.com/dotnet/runtime/issues/36063

I do not see why I'm must live with the code duplication for sync and async method or using hacks.

Conclusion: Create Task manually and control them is much better. Handler to Task give more control. We can monitor Tasks and manage them:

https://github.com/lsmolinski/MonitoredQueueBackgroundWorkItem

Sorry for my english.