HttpClient在async中产生的代码不执行和堵塞

折腾好久,最终在这里找到了答案: https://stackoverflow.com/questions/10343632/httpclient-getasync-never-returns-when-using-await-async/10351400#10351400

先来看看案例一,代码如下:

public class ValuesController : ApiController
    {
        [Route("getMessage")]
        public string GetMessage()
        {
            NetTask();
            return "Ok";
        }

        public async Task NetTask()
        {
            using (HttpClient client = new HttpClient())
            {
                HttpRequestMessage request = new HttpRequestMessage
                {
                    Method = HttpMethod.Get
                };
                request.RequestUri = new Uri("http://www.baidu.com");

                var response = await client.SendAsync(request);
                var result = await response.Content.ReadAsStringAsync();
                Debug.WriteLine(result);
            }
        }
    }

 

上面这段代码,当我们在浏览器上访问/getMessage时,永远不会输出Debug.WriteLine(result); 

猜想是不是NetTask()没有堵塞,导致主线程结束后,NetTask()方法中的异步代码没有执行。

运行案例二,把NetTask()堵塞掉,代码如下:

public string GetMessage()
        {
            NetTask().GetAwaiter().GetResult();
            return "Ok";
        }

 

访问/getMessage,发现请求得不到响应,断点一下,发现NetTask()方法一直被堵塞了,引发了死锁!

有的人可能会说GetMessage()应该打上async标记,使用await 等待NetTask,没错,这样实现的话代码是没有任何问题的,但是不满足我们现在的要求,假设NetTask()的网络请求耗时为10秒,如果我们这样去写我们的代码,那GetMessage()这个Action的耗时起码需要10秒。

也有人会推荐使用Task.Run,用其他线程去执行这个耗时请求。但是这样的操作还是额外产生了线程的调度(主线程,Task.Run中的线程,await后的线程,共3个线程),我们的理想操作是主线程完成请求前的所有动作后不堵塞完成NetTask()后面的代码。请求完成后的操作交给其他线程。这样我们只使用到2个线程。

Context 上下文

在我们使用async/await时,我们首先需要了解"Context",简单的说,当我们使用async/await时,当程序遇到需要等待的地方,程序会捕捉到当前的Context,当异步方法完成后,程序会恢复捕捉到的Context,并在上面执行后续的代码。异步的上下文可以分为UI上下文,ASP.NET Request上下文,线程池上下文,让我们用代码解释一下:

// WinForms example (WPF同原理).
private async void DownloadFileButton_Click(object sender, EventArgs e)
{
  // 异步方法,当代码需要await时,程序捕捉当前的上下文,此处为UI上下文。await期间主线程是不会被阻塞的
  await DownloadFileAsync(fileNameTextBox.Text);

  // await完成,程序将为我们恢复捕捉的上下文,也就是UI上下文,因为我们拥有UI上下文所以可以更新UI控件,即使当前的线程可能是其他线程
  resultTextBox.Text = "File downloaded!";
}

// ASP.NET example
protected async void MyButton_Click(object sender, EventArgs e)
{
  // 异步方法,当代码需要await时,程序捕捉当前的上下文,此处为请求的上下文。await期间允许线程继续处理其他请求
  await DownloadFileAsync(...);

  // await完成,程序恢复request上下文,即便当前操作的可能是其他线程,但我们拥有请求的上下文,所以我们能响应请求。
  Response.Write("File downloaded!");
}

 那么再回过头来解释一下案例一为什么不能运行到client.SendAsync()后面的代码,按照我们刚才所说,异步时会为我们捕捉请求的上下文。在看NetTask()并没有被阻塞,所以请求马上就结束了。当client.SendAsync()被执行完后无法操作一个已经结束的请求上下文,自然不会执行下面的代码。当然如果client.SendAsync()执行的够快,在请求还没有结束前完成,那么依旧能运行到后续的代码。

再来解释一下案例二为什么会死锁,在案例二中,使用GetAwaiter().GetResult()阻塞住了主线程,异步帮我们捕捉了请求的上下文,当异步完成恢复请求上下文的环境时,因为主线程阻塞等待异步的完成,而异步线程因为主线程阻塞没办法操作请求上下文,产生了死锁。

那么应该怎么解决这种问题呢?

 ConfigureAwait

大部分情况,我们可能不需要返回到"主"上下文中,大部分的异步方法可以被组合使用,每个异步操作可能只代表本身,和之前的上下文并没有什么联系。此时,我们可以通过ConfigureAwait告诉程序不需要捕捉上下文:

 

private async Task DownloadFileAsync(string fileName)
{
  // 使用HttpClient或者其他下载文件
  var fileContents = await DownloadFileContentsAsync(fileName).ConfigureAwait(false);

  // 因为配置了ConfigureAwait(false),所以此时不再是之前的上下文,而是Thread pool Context// 将文件写到硬盘
  await WriteToDiskAsync(fileName, fileContents).ConfigureAwait(false);

  // 第二个ConfigureAwait不是必须的,但是不错的做法
}

// WinForms example (it works exactly the same for WPF).
private async void DownloadFileButton_Click(object sender, EventArgs e)
{
  // await,UI线程不会在此阻塞,并且调用异步方法没有使用ConfigureAwait(false),所以此时会捕捉UI上下文
  await DownloadFileAsync(fileNameTextBox.Text);

  // 我们拥有UI上下文,所以当异步恢复时,可以直接操作UI控件
  resultTextBox.Text = "File downloaded!";
}

 当然,如果我们操作全程都包括标注了async,也不会出现阻塞的情况,但Action肯定是需要等异步结束后才能返回的。

 

参考文章:https://blogs.msdn.microsoft.com/pfxteam/2012/04/12/asyncawait-faq/

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

    http://blog.stephencleary.com/2012/02/async-and-await.html

 

posted @ 2018-07-10 21:40  没有头的狗会死  阅读(4048)  评论(0编辑  收藏  举报