ChatGPT解决这个技术问题 Extra ChatGPT

An async/await example that causes a deadlock

I came across some best practices for asynchronous programming using c#'s async/await keywords (I'm new to c# 5.0).

One of the advices given was the following:

Stability: Know your synchronization contexts

... Some synchronization contexts are non-reentrant and single-threaded. This means only one unit of work can be executed in the context at a given time. An example of this is the Windows UI thread or the ASP.NET request context. In these single-threaded synchronization contexts, it’s easy to deadlock yourself. If you spawn off a task from a single-threaded context, then wait for that task in the context, your waiting code may be blocking the background task.

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

If I try to dissect it myself, the main thread spawns to a new one in MyWebService.GetDataAsync();, but since the main thread awaits there, it waits on the result in GetDataAsync().Result. Meanwhile, say the data is ready. Why doesn't the main thread continue it's continuation logic and returns a string result from GetDataAsync() ?

Can someone please explain me why there is a deadlock in the above example? I'm completely clueless about what the problem is ...

Are you really sure that GetDataAsync finishes it's stuff? Or it gets stuck causing just lock and not deadlock?
This is the example that was provided. To my understanding it should finish it's stuff and have a result of some sort ready ...
Why are you even waiting for the task? You should be awaiting instead because you basically lost all benefits of async model.
To add to @ToniPetrina's point, even w/o the deadlock problem, var data = GetDataAsync().Result; is a line of code that should never be done in a context that you should not block (UI or ASP.NET request). Even if it doesn't deadlock, it is blocking the thread an indeterminate amount of time. So basically its a terrible example. [You need to get off of the UI thread before executing code like that, or use await there also, as Toni suggests.]

s
sritmak

Take a look at this example, Stephen has a clear answer for you:

So this is what happens, starting with the top-level method (Button1_Click for UI / MyController.Get for ASP.NET): The top-level method calls GetJsonAsync (within the UI/ASP.NET context). GetJsonAsync starts the REST request by calling HttpClient.GetStringAsync (still within the context). GetStringAsync returns an uncompleted Task, indicating the REST request is not complete. GetJsonAsync awaits the Task returned by GetStringAsync. The context is captured and will be used to continue running the GetJsonAsync method later. GetJsonAsync returns an uncompleted Task, indicating that the GetJsonAsync method is not complete. The top-level method synchronously blocks on the Task returned by GetJsonAsync. This blocks the context thread. ... Eventually, the REST request will complete. This completes the Task that was returned by GetStringAsync. The continuation for GetJsonAsync is now ready to run, and it waits for the context to be available so it can execute in the context. Deadlock. The top-level method is blocking the context thread, waiting for GetJsonAsync to complete, and GetJsonAsync is waiting for the context to be free so it can complete. For the UI example, the "context" is the UI context; for the ASP.NET example, the "context" is the ASP.NET request context. This type of deadlock can be caused for either "context".

Another link you should read: Await, and UI, and deadlocks! Oh my!


R
Richard Garside

Fact 1: GetDataAsync().Result; will run when the task returned by GetDataAsync() completes, in the meantime it blocks the UI thread

Fact 2: The continuation of the await (return result.ToString()) is queued to the UI thread for execution

Fact 3: The task returned by GetDataAsync() will complete when its queued continuation is run

Fact 4: The queued continuation is never run, because the UI thread is blocked (Fact 1)

Deadlock!

The deadlock can be broken by provided alternatives to avoid Fact 1 or Fact 2.

Fix 1: Avoid 1,4. Instead of blocking the UI thread, use var data = await GetDataAsync(), which allows the UI thread to keep running

Fix 2: Avoid 2,3. Queue the continuation of the await to a different thread that is not blocked, e.g. use var data = Task.Run(GetDataAsync).Result, which will post the continuation to the sync context of a threadpool thread. This allows the task returned by GetDataAsync() to complete.

This is explained really well in an article by Stephen Toub, about half way down where he uses the example of DelayAsync().


Regarding, var data = Task.Run(GetDataAsync).Result, that's new to me. I always thought the outer .Result will be readily available as soon as the first await of GetDataAsync is hit, so data will always be default. Interesting.
C
Carl Heinrich Hancke

I was just fiddling with this issue again in an ASP.NET MVC project. When you want to call async methods from a PartialView, you're not allowed to make the PartialView async. You'll get an exception if you do.

You can use the following simple workaround in the scenario where you want to call an async method from a sync method:

Before the call, clear the SynchronizationContext Do the call, there will be no more deadlock here, wait for it to finish Restore the SynchronizationContext

Example:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}

m
marvelTracker

Another main point is that you should not block on Tasks, and use async all the way down to prevent deadlocks. Then it will be all asynchronous not synchronous blocking.

public async Task<ActionResult> ActionAsync()
{

    var data = await GetDataAsync();

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

What if I want the main (UI) thread to be blocked until the task finishes? Or in a Console app for example? Let's say I want to use HttpClient, which only supports async... How do I use it synchronously without the risk of deadlock? This must be possible. If WebClient can be used that way (because of having sync methods) and works perfectly, then why couldn't it be done with HttpClient too?
See answer from Philip Ngan above (i know this was posted after this comment): Queue the continuation of the await to a different thread that is not blocked, e.g. use var data = Task.Run(GetDataAsync).Result
@Dexter - re "What if I want the main (UI) thread to be blocked until the task finishes?" - do you truly want the UI thread blocked, meaning user can't do anything, can't even cancel - or is it that you don't want to continue the method you are in? "await" or "Task.ContinueWith" handle the latter case.
@ToolmakerSteve of course I don't want to continue the method. But I simply cannot use await because I cannot use async all the way either - HttpClient is invoked in main, which of course cannot be async. And then I mentioned doing all this in a Console app - in this case I want exactly the former - I don't want my app to even be multi-threaded. Block everything.
O
Orace

A work around I came to is to use a Join extension method on the task before asking for the result.

The code look's like this:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

Where the join method is:

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

I'm not enough into the domain to see the drawbacks of this solution (if any)


I like this, dispatching actions work great in Redux. I am learning c# and want to know, what's wrong with this and does it work? Why was this downvoted?