UI 程序示例
参考下面的代码。一个按钮点击事件会发起一个 REST 调用,并将结果显示在一个 TextBox 中:
public static async Task<JObject> GetJsonAsync(Uri uri)
{
using (var client = new HttpClient())
{
var jsonString = await client.GetStringAsync(uri);
return JObject.Parse(jsonString);
}
}
public void Buttion1_Click(...)
{
var jsonTask = GetJsonAsync(...);
textBox1.Text = jsonTask.Result;
}
"GetJson" 方法负责发起实际的 REST 请求,并将结果转换为 JSON 对象。而按钮点击处理事件等待 "GetJson" 方法完成,然后显示它的结果。
这段代码将会引起死锁。
ASP.NET 示例
接下来是一个非常简单的示例。我们拥有一个同样的类库方法,它执行一个 REST 请求,只是这一次它被用于 ASP.NET 上下文环境中:
public static async Task<JObject> GetJsonAsync(Uri uri)
{
using (var client = new HttpClient())
{
var jsonString = await client.GetStringAsync(uri);
return JObject.Parse(jsonString);
}
}
public class MyController : ApiController
{
public string Get()
{
var jsonTask = GetJsonAsync(...);
return jsonTask.Result.ToString();
}
}
基于同样的原因,这段代码也会造成死锁。
是什么造成了死锁
在第一个示例中,当前上下文是 UI 上下文,在第二个示例中,当前上下文则是 ASP.NET 请求上下文(请注意:一个 ASP.NET 请求并不会关联到一个特定的线程,但是同一个请求在同一时刻只会在一个线程中执行)
因此,这就是以上示例代码执行的结果:
- 最顶层的方法调用了 GetJsonAsync 方法(在 UI/ASP.NET 上下文中)
- GetJsonAsync 方法通过调用 HttpClient.GetStringAsync 发起一个 REST 请求(仍然在同一个上下文中)
- GetStringAsync 方法返回一个未完成的 Task,表明 REST 请求还未完成
- 接下来 GetJsonAsync 方法将等待 GetStringAsync 返回的任务。此时当前上下文将被捕获,用以在 GetStringAsync 完成后继续执行 GetJsonAsync 方法。GetJsonAsync 返回了一个未完成的任务,指示该方法并未完成
- 顶层方法此时将同步阻塞由 GetJsonAsync 方法返回的 Task,这将会阻塞当前线程上下文
- 最终,REST 请求将会完成。这会导致由 GetStringAsync 所返回的任务完成
- 此时 GetJsonAsync 的后续代码将准备开始执行,此时它将等待当前线程上下文可用,以继续执行下去
- 此时死锁产生。顶层方法阻塞了当前上下文,等待 GetJsonAsync 方法完成,而 GetJsonAsync 方法却又在等待当前上下文可用,让它可以完成并继续。
避免死锁
这里有2种方法可以避免死锁:
- 无论何时,在 async 方法中使用 ConfigureAwait(false)
- 不要阻塞 Task,无论何时都使用 async
针对第一种方法,新的 GetJsonAsync 方法看起来会像这样:
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);
}
}
这将会修改 GetJsonAsync 方法的等待行为,此时它将不会在当前上下文中恢复执行,而是在线程池中抓取一个线程直接继续执行。这避免了 GetJsonAsync 方法重新进入(re-enter)当前上下文线程
针对第二种方法,此时的顶层方法看起来会像这样:
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();
}
}
这将修改顶层方法的等待行为,所有的等待都将是异步等待
参考资料
Best practice to call ConfigureAwait for all server-side code
Don't Block on Async Code
网友评论