1. 程式人生 > 程式設計 >淺談C# async await 死鎖問題總結

淺談C# async await 死鎖問題總結

可能發生死鎖的程式型別

1、WPF/WinForm程式

2、asp.net (不包括asp.net core)程式

死鎖的產生原理

對非同步方法返回的Task呼叫Wait()或訪問Result屬性時,可能會產生死鎖。

下面的WPF程式碼會出現死鎖:

private void Button_Click_7(object sender,RoutedEventArgs e)
    {
      Method1().Wait();
    }

    private async Task Method1()
    {
      await Task.Delay(100);

      txtLog.AppendText("後續程式碼");
    }

下面的asp.net mvc程式碼也會出現死鎖:

public ActionResult Index()
    {
      string s=Method1().Result;

      return View();
    }

    private async Task<string> Method1()
    {
      await Task.Delay(100);

      return "hello";
    }

以WPF程式碼為例,事件處理器呼叫Method1,得到Task物件,然後呼叫Task的Wait方法,阻塞自己所在的執行緒,即主執行緒,直到Task物件“完成”。而返回的Task物件要想“完成”,必須在主執行緒上執行await之後的程式碼。而主執行緒早就處於阻塞狀態,它在等待Task物件完成!於是死鎖就產生了。

asp.net mvc程式碼是同樣的道理。

什麼時候必然會死鎖,如何避免

從上面的兩個例子中似乎可以得出結論:在WPF/WinForm/asp.net程式中,在非同步方法上呼叫.Result/Wait(),就會產生死鎖。然而事實並非如此。

如下面的WPF程式碼就不會出現死鎖:(從web獲取資料並顯示在文字框中。此程式碼僅為舉例說明,非同步事件處理器才是正道)

private void Button_Click_8(object sender,RoutedEventArgs e)
    {
      HttpClient httpClient = new HttpClient();
      httpClient.BaseAddress = new Uri("https://www.baidu.com/");

      string html = httpClient.GetStringAsync("/").Result;      html = "【" + html + "】";

      txtLog.AppendText(html);
    }

把獲取資料的程式碼摘出來吧:

private void Button_Click_8(object sender,RoutedEventArgs e)
    {
      string html = GetHtml();

      txtLog.AppendText(html);
    }

    private string GetHtml()
    {
      HttpClient httpClient = new HttpClient();
      httpClient.BaseAddress = new Uri("https://www.baidu.com/");

      string html=httpClient.GetStringAsync("/").Result;       html = "【" + html + "】";             return html;
    }

完全沒問題,這是肯定的。

GetHtml()可以寫成非同步方法,再改一下:

private void Button_Click_8(object sender,RoutedEventArgs e)
    {
      string html = GetHtml().Result;

      txtLog.AppendText(html);
    }

    private async Task<string> GetHtml()
    {
      HttpClient httpClient = new HttpClient();
      httpClient.BaseAddress = new Uri("https://www.baidu.com/");
      string html=await httpClient.GetStringAsync("/");              
      html = "【" + html + "】";
      return html;    }

(HttpClient的GetStringAsync()方法是非同步方法,我們呼叫它,然後用async/await的方式建立了一個自己的非同步方法。先不“一路非同步到底(Async All the Way)”。)

執行一下,死鎖出現了。

為什麼在HttpClient的GetStringAsync()方法上執行.Result不會死鎖,而在自己寫的非同步方法上執行.Result,就出現了死鎖?難道HttpClient的GetStringAsync()方法內部有什麼特殊的處理?

看一下mono的HttpClient原始碼,可以發現:

所有await 表示式後面,都加了ConfigureAwait (false),如

return await resp.Content.ReadAsStringAsync ().ConfigureAwait (false);

而由Task的msdn文件可以知,ConfigureAwait (false)會指示await之後的程式碼不在原先的context (可理解為執行緒)上執行。

修改一下GetHtml()非同步方法的程式碼:

private void Button_Click_8(object sender,RoutedEventArgs e)
    {
      string html = GetHtml().Result;

      txtLog.AppendText(html);
    }
    private async Task<string> GetHtml()
    {
      HttpClient httpClient = new HttpClient();
      httpClient.BaseAddress = new Uri("https://www.baidu.com/");
      string html=await httpClient.GetStringAsync("/").ConfigureAwait(false);              
      html = "【" + html + "】";
      return html;    }

可以發現,死鎖不會出現了。

分析:GetHtml()被呼叫後,主執行緒阻塞,等待Task物件“完成”;HttpClient獲取資料完畢,在另外的執行緒上執行了await的之後的程式碼,於是Task物件完成。主執行緒恢復執行。(注意,即使“await之後沒有程式碼”,即GetHtml()方法體中直接寫return await httpClient.GetStringAsync("/"),也是需要加.ConfigureAwait(false)的)

當然,如果事件處理器是非同步的,即使不加.ConfigureAwait(false),也不會有任何問題:

private async void Button_Click_8(object sender,RoutedEventArgs e)
    {
      string html = await GetHtml();

      txtLog.AppendText(html);
    }
    private async Task<string> GetHtml()
    {
      HttpClient httpClient = new HttpClient();
      httpClient.BaseAddress = new Uri("https://www.baidu.com/");
      string html = await httpClient.GetStringAsync("/");
      html = "【" + html + "】";
      return html;
    }

試想一下,如果GetHtml()被放到單獨的類中,做成類庫,那麼,裡面如果不加.ConfigureAwait(false),則只能假設使用這個類庫的人嚴格遵循非同步程式設計規範了。一旦使用者在GetHtml()上執行.Result,死鎖就無可避免了。

仔細看HttpClient的原始碼,可以發現,它的GetStringAsync()方法也並不是“天生的”非同步方法,它也是用await運算子呼叫了自己的其他的非同步方法,並且在每次呼叫後都添加了.ConfigureAwait(false)。

那麼,最初的WPF程式的死鎖是否可以用.ConfigureAwait(false)解決呢?注意,txtLog是一個文字框,UI控制元件只能在UI執行緒訪問,所以新增上.ConfigureAwait(false)後會報錯:“InvalidOperationException: 呼叫執行緒無法訪問此物件,因為另一個執行緒擁有該物件”。那麼是否可以改成這樣:

private void Button_Click_7(object sender,RoutedEventArgs e)
    {
      Method1().Wait();
    }

    private async Task Method1()
    {
      await Task.Delay(100).ConfigureAwait(false);

      Dispatcher.Invoke(() => {
        txtLog.AppendText("後續程式碼");
      });
    }

依然是死鎖。所以,乖乖的用非同步事件處理器吧:

private async void Button_Click_7(object sender,RoutedEventArgs e)
    {
      await Method1();
    }

    private async Task Method1()
    {
      await Task.Delay(100);

      txtLog.AppendText("後續程式碼");
    }

上面的程式碼還說明一個問題:在非同步工具方法中,不要寫訪問UI控制元件的程式碼,否則無法規避死鎖問題。

總結

  • 死鎖會發生在不遵循非同步程式設計規範——在非同步方法返回的Task物件上執行Wait()或.Result時
  • ConfigureAwait(false)指定await後的程式碼不返回原先的context,可以避免死鎖
  • 如果await之後的程式碼不需要返回原先的context執行,例如,僅僅是執行Http請求,獲取和處理資料,那麼完全可以加上ConfigureAwait(false)。
  • 如果作為類庫的創作者,編寫非同步方法時,應儘可能的使用ConfigureAwait(false),以保證一旦類庫的使用者阻塞非同步方法時,不會產生死鎖。
  • 在非同步類庫/工具方法中,應避免加入訪問UI控制元件的程式碼

附加 async/await學習資料

C# Under the Hood: async/await 作者從動手寫一個“可等待”的方法開始,進而通過反編譯工具分析非同步方法生成的的實質程式碼,揭示了async/await的本質——回撥

What happens in an async method msdn程式設計指南,圖示非同步方法的執行流程

到此這篇關於淺談C# async await 死鎖問題總結的文章就介紹到這了,更多相關C# async await 死鎖內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!