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控件的代碼,否則無法規避死鎖問題。

 

總結

  1. 死鎖會發生在不遵循異步編程規范——在異步方法返回的Task對象上執行Wait().Result時

  2. ConfigureAwait(false)指定await后的代碼不返回原先的context,可以避免死鎖
  3. 如果await之后的代碼不需要返回原先的context執行,例如,僅僅是執行Http請求,獲取和處理數據,那么完全可以加上ConfigureAwait(false)

  4. 如果作為類庫的創作者,編寫異步方法時,應盡可能的使用ConfigureAwait(false),以保證一旦類庫的使用者阻塞異步方法時,不會產生死鎖。
  5. 在異步類庫/工具方法中,應避免加入訪問UI控件的代碼

 

附加  async/await學習資料

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

What happens in an async method  msdn編程指南,圖示異步方法的執行流程

 


免責聲明!

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



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