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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM