[Turn] Beware of deadlock caused by await and async mode in C# 5.0

Original link

https://www.cnblogs.com/OpenCoder/p/4434574.html

content

UI Example

Consider the example below. A button click will initiate a REST call and display the results in a text box (this sample is for Windows Forms, but the same principles apply to any UI application).

copy code
// My "library" method.
public static async Task<JObject> GetJsonAsync(Uri uri)
{
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri);
    return JObject.Parse(jsonString);
  }
}

// My "top-level" method.
public void Button1_Click(...)
{
  var jsonTask = GetJsonAsync (...);
  textBox1.Text = jsonTask.Result;
}
copy code

The “GetJson” helper method takes care of making the actual REST call and parsing it as JSON. The button click handler waits for the helper method to complete and then displays its results.

This code will deadlock.

 

 

ASP.NET Example

This example is very similar; we have a library method that performs a REST call, only this time it’s used in an ASP.NET context (Web API in this case, but the same principles apply to any ASP.NET application):

copy code
// My "library" method.
public static async Task<JObject> GetJsonAsync(Uri uri)
{
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri);
    return JObject.Parse(jsonString);
  }
}

// My "top-level" method.
public class MyController : ApiController
{
  public string Get()
  {
    var jsonTask = GetJsonAsync (...);
    return jsonTask.Result.ToString();
  }
}
copy code

This code will also deadlock. For the same reason.

 

 

 

What Causes the Deadlock

Here’s the situation: remember from my intro post that after you await a Task, when the method continues it will continue in a context.

In the first case, this context is a UI context (which applies to any UI except Console applications). In the second case, this context is an ASP.NET request context.

One other important point: an ASP.NET request context is not tied to a specific thread (like the UI context is), but it doesonly allow one thread in at a time. This interesting aspect is not officially documented anywhere AFAIK, but it is mentioned in my MSDN article about SynchronizationContext.

So this is what happens, starting with the top-level method (Button1_Click for UI / MyController.Get for ASP.NET):

  1. The top-level method calls GetJsonAsync (within the UI/ASP.NET context).
  2. GetJsonAsync starts the REST request by calling HttpClient.GetStringAsync (still within the context, within the context here means that the GetJsonAsync method still uses the thread that executes the top-level method to execute, that is, the main thread).
  3. GetStringAsync returns an uncompleted Task, indicating the REST request is not complete.
  4. GetJsonAsync awaits the Task returned by GetStringAsync. The context (the context here still refers to the thread that executes the top-level method. When the GetStringAsync method returns after execution, GetJsonAsync will continue to use the thread that executes the top-level method to execute the await keyword. The code after that, which is what causes the code to deadlock in this example) 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.
  5. The top-level method synchronously blocks on the Task returned by GetJsonAsync. This blocks the context thread.
  6. … Eventually, the REST request will complete. This completes the Task that was returned by GetStringAsync.
  7. 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.
  8. 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”.

The general meaning of the above is that when using the await and async mode, the code block after the await keyword line will be executed by a context (that is, the ASP.NET request context and UI context mentioned above) thread. If we The thread that calls the top-level method in this example is called thread A (that is, the context thread). Since the GetJsonAsync method is also called by thread A, when the GetStringAsync method of await in the GetJsonAsync method is executed, GetJsonAsync needs to reuse thread A. Execute the code after the await code line, and now because thread A is blocked in the code of the top-level method because it accesses jsonTask.Result (because when thread A calls jsonTask.Result in the top-level method code, the GetStringAsync of await The Task has not been executed yet, so it is blocked by thread A), so GetJsonAsync cannot reuse the code block after thread A to execute the await code line, and it is also blocked, so a deadlock is formed. That is to say, thread A in the top-level method code is blocked because it waits for the GetStringAsync of the await in GetJsonAsync to end, and GetStringAsync also waits for the end of the blocking of thread A in the top-level method to obtain thread A to execute the code after the await code line in GetJsonAsync. Blocked, the two blocks wait for each other, deadlocking each other.

Preventing the Deadlock

There are two best practices (both covered in my intro post) that avoid this situation:

  1. In your “library” async methods, use ConfigureAwait(false) wherever possible.
  2. Don’t block on Tasks; use async all the way down.
  3. If you want to end the invocation of async & await mode, start a new thread to await the return result of the asynchronous method

Here I will add that if you are developing a Winform program, it is best to use the second method to avoid deadlocks, that is, do not block the main thread, so that when the Task object thread waiting for await is executed, the main thread is not blocked. , so the code behind the await will continue at the right time (the "appropriate time" mentioned here is judged by the .Net Framework itself, the .Net Framework will arrange for the main thread to continue executing the code behind the await at some point) Executed on the main thread. The reason why the first method is not recommended in Winform is because the first method will cause the code behind the await to be executed on another thread instead of the main thread. If there is code after the await to set the Winform control The value of , then it will cause the thread safety problem of Winform program, so the best way in Winform is not to block the main thread, so that the code behind await can be executed on the main thread. But in Asp.net, the first or second method above can be used, and there is no thread safety problem.

Consider the first best practice. The new “library” method looks like this:

copy code
public static async Task<JObject> GetJsonAsync(Uri uri)
{
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri).ConfigureAwait(false);
    return JObject.Parse(jsonString);
  }
}
copy code

This changes the continuation behavior of GetJsonAsync so that it does not resume on the context. Instead, GetJsonAsync will resume on a thread pool thread. This enables GetJsonAsync to complete the Task it returned without having to re-enter the context.

Consider the second best practice. The new “top-level” methods look like this:

copy code
public async void Button1_Click(...)
{
  var json = await GetJsonAsync(...);
  textBox1.Text = json;
}

public class MyController : ApiController
{
  public async Task<string> Get()
  {
    var json = await GetJsonAsync(...);
    return json.ToString();
  }
}
copy code

This changes the blocking behavior of the top-level methods so that the context is never actually blocked; all “waits” are “asynchronous waits”.

Note: It is best to apply both best practices. Either one will prevent the deadlock, but both must be applied to achieve maximum performance and responsiveness.

The third best practice: If you want to end the invocation of async & await mode, start a new thread to await the return result of the asynchronous method

copy code
// My "library" method.
public static async Task<JObject> GetJsonAsync(Uri uri)
{
    using (var client = new HttpClient())
    {
        var jsonString = await client.GetStringAsync(uri);
        return JObject.Parse(jsonString);
    }
}

// My "top-level" method.
public string Get()
{
    string jsonResultString = string.Empty;

    Task.Run(async () =>
    {
        jsonResultString = await GetJsonAsync(...);
    }).Wait();//The thread is started here to prevent deadlock caused by Async & Await mode

    return jsonResultString;
}
copy code

In this way, because the GetJsonAsync method is called by the thread newly started by Task.Run, after await GetJsonAsync(...) is executed, the .Net Framework will use the thread newly started by Task.Run to execute the code after await. It will not block each other with the thread of the top-level method (that is, the context thread), causing deadlock.

 

Finally, I would like to add that the await and async deadlock problems mentioned in this article do not exist in .Net console programs. Because the experiment found that in the .Net console program, the code after the await keyword is executed on a new thread by default, that is to say, even if Task.ConfigureAwait(false) is not called in the console program, await The code after the keyword line will also be executed on a newly started thread and will not deadlock with the main thread. But deadlock occurs in Winform and Asp.net.

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325249037&siteId=291194637