双魚蓮花 發表於 2020-1-5 00:12:00

C# async await 死锁问题总结

<p><strong>可能发生死锁的程序类型</strong></p>
<p>1、WPF/WinForm程序</p>
<p>2、asp.net (不包括asp.net core)程序</p>
<p>&nbsp;</p>
<p><strong>死锁的产生原理</strong></p>
<p>对异步方法返回的Task调用Wait()或访问Result属性时,可能会产生死锁。</p>
<p>下面的WPF代码会出现死锁:</p>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> Button_Click_7(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, RoutedEventArgs e)
      {
            Method1().Wait();
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">async</span><span style="color: rgba(0, 0, 0, 1)"> Task Method1()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">await</span> Task.Delay(<span style="color: rgba(128, 0, 128, 1)">100</span><span style="color: rgba(0, 0, 0, 1)">);

            txtLog.AppendText(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">后续代码</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
      }</span></pre>
</div>
<p>下面的asp.net mvc代码也会出现死锁:</p>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> ActionResult Index()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">string</span> s=<span style="color: rgba(0, 0, 0, 1)">Method1().Result;

            </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> View();
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">async</span> Task&lt;<span style="color: rgba(0, 0, 255, 1)">string</span>&gt;<span style="color: rgba(0, 0, 0, 1)"> Method1()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">await</span> Task.Delay(<span style="color: rgba(128, 0, 128, 1)">100</span><span style="color: rgba(0, 0, 0, 1)">);

            </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">hello</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
      }</span></pre>
</div>
<p>以WPF代码为例,事件处理器调用Method1,得到Task对象,然后调用Task的Wait方法,阻塞自己所在的线程,即主线程,直到Task对象“完成”。而返回的Task对象要想“完成”,必须在主线程上执行await之后的代码。而主线程早就处于阻塞状态,它在等待Task对象完成!于是死锁就产生了。</p>
<p>asp.net mvc代码是同样的道理。</p>
<p>&nbsp;</p>
<p><strong>什么时候必然会死锁,如何避免</strong></p>
<p>从上面的两个例子中似乎可以得出结论:在WPF/WinForm/asp.net程序中,在异步方法上调用.Result/Wait(),就会产生死锁。然而事实并非如此。</p>
<p>如下面的WPF代码就不会出现死锁:(从web获取数据并显示在文本框中。此代码仅为举例说明,异步事件处理器才是正道)</p>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> Button_Click_8(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, RoutedEventArgs e)
      {
            HttpClient httpClient </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> HttpClient();
            httpClient.BaseAddress </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> Uri(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">https://www.baidu.com/</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);

            </span><span style="color: rgba(0, 0, 255, 1)">string</span> html = httpClient.GetStringAsync(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">).Result;<br><br>            html = "【" + html + "】";

            txtLog.AppendText(html);
      }</span></pre>
</div>
<p>把获取数据的代码摘出来吧:</p>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> Button_Click_8(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, RoutedEventArgs e)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">string</span> html =<span style="color: rgba(0, 0, 0, 1)"> GetHtml();

            txtLog.AppendText(html);
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)"> GetHtml()
      {
            HttpClient httpClient </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> HttpClient();
            httpClient.BaseAddress </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> Uri(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">https://www.baidu.com/</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);

            string html=</span>httpClient.GetStringAsync(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">).Result;<br><br>       html = "【" + html + "】";<br>       <br>            return html;
      }</span></pre>
</div>
<p>完全没问题,这是肯定的。</p>
<p>GetHtml()可以写成异步方法,再改一下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">      private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> Button_Click_8(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, RoutedEventArgs e)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">string</span> html =<span style="color: rgba(0, 0, 0, 1)"> GetHtml().Result;

            txtLog.AppendText(html);
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">async</span> Task&lt;<span style="color: rgba(0, 0, 255, 1)">string</span>&gt;<span style="color: rgba(0, 0, 0, 1)"> GetHtml()
      {
            HttpClient httpClient </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> HttpClient();
            httpClient.BaseAddress </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> Uri(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">https://www.baidu.com/</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);</span></pre>
<pre><span>            string html=</span>await httpClient.GetStringAsync(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/</span><span style="color: rgba(128, 0, 0, 1)">"</span><span>);              </span></pre>
<pre><span>            html = "【" + html + "】";</span></pre>
<pre><span>           return html;<br>      }</span></pre>
</div>
<p>(HttpClient的GetStringAsync()方法是异步方法,我们调用它,然后用async/await的方式创建了一个自己的异步方法。先不“一路异步到底(Async All the Way)”。)</p>
<p>运行一下,死锁出现了。</p>
<p>为什么在HttpClient的GetStringAsync()方法上执行.Result不会死锁,而在自己写的异步方法上执行.Result,就出现了死锁?难道HttpClient的GetStringAsync()方法内部有什么特殊的处理?</p>
<p>看一下mono的HttpClient源代码,可以发现:</p>
<p>所有await 表达式后面,都加了ConfigureAwait (<span class="pl-c1">false)</span>,如</p>
<blockquote>
<p><span class="pl-k">return <span class="pl-k">await <span class="pl-smi">resp.<span class="pl-smi">Content.<span class="pl-en">ReadAsStringAsync ().<span class="pl-en">ConfigureAwait (<span class="pl-c1">false);</span></span></span></span></span></span></span></p>
</blockquote>
<p>而由Task的msdn文档可以知,ConfigureAwait (<span class="pl-c1">false)会指示await之后的代码不在原先的context (可理解为线程)上运行。</span></p>
<p><span class="pl-c1">修改一下GetHtml()异步方法的代码:</span></p>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> Button_Click_8(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, RoutedEventArgs e)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">string</span> html =<span style="color: rgba(0, 0, 0, 1)"> GetHtml().Result;

            txtLog.AppendText(html);
      }<br><br></span></pre>
<pre><span>      private async Task&lt;string&gt;<span> GetHtml()
      {
            HttpClient httpClient = new<span> HttpClient();
            httpClient.BaseAddress = new Uri("https://www.baidu.com/"<span>);</span></span></span></span></pre>
<pre>            string html=await httpClient.GetStringAsync("/")<strong>.ConfigureAwait(false)</strong>;              </pre>
<pre>            html = "【" + html + "】";</pre>
<pre>           return html;<br>      }</pre>
<pre><span style="color: rgba(0, 0, 0, 1)">&nbsp;</span></pre>
</div>
<p>可以发现,死锁不会出现了。</p>
<p>分析:GetHtml()被调用后,主线程阻塞,等待Task对象“完成”;HttpClient获取数据完毕,在另外的线程上执行了await的之后的代码,于是Task对象完成。主线程恢复执行。(注意,即使“await之后没有代码”,即GetHtml()方法体中直接写return await httpClient.GetStringAsync("/"),也是需要加.ConfigureAwait(false)的)</p>
<p>当然,如果事件处理器是异步的,即使不加.ConfigureAwait(false),也不会有任何问题:</p>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">async</span> <span style="color: rgba(0, 0, 255, 1)">void</span> Button_Click_8(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, RoutedEventArgs e)
      {
            </span><span style="color: rgba(0, 0, 255, 1)">string</span> html = <span style="color: rgba(0, 0, 255, 1)">await</span><span style="color: rgba(0, 0, 0, 1)"> GetHtml();

            txtLog.AppendText(html);
      }<br>
      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">async</span> Task&lt;<span style="color: rgba(0, 0, 255, 1)">string</span>&gt;<span style="color: rgba(0, 0, 0, 1)"> GetHtml()
      {
            HttpClient httpClient </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> HttpClient();
            httpClient.BaseAddress </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> Uri(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">https://www.baidu.com/</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
            </span><span style="color: rgba(0, 0, 255, 1)">string</span> html = <span style="color: rgba(0, 0, 255, 1)">await</span> httpClient.GetStringAsync(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/</span><span style="color: rgba(128, 0, 0, 1)">"</span>)<span style="color: rgba(0, 0, 0, 1)">;
            html </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">【</span><span style="color: rgba(128, 0, 0, 1)">"</span> + html + <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">】</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
            </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> html;
      }</span></pre>
</div>
<p>试想一下,如果GetHtml()被放到单独的类中,做成类库,那么,里面如果不加.ConfigureAwait(false),则只能假设使用这个类库的人严格遵循异步编程规范了。一旦使用者在GetHtml()上执行.Result,死锁就无可避免了。</p>
<p>仔细看HttpClient的源代码,可以发现,它的GetStringAsync()方法也并不是“天生的”异步方法,它也是用await运算符调用了自己的其他的异步方法,并且在每次调用后都添加了.ConfigureAwait(false)。</p>
<p>&nbsp;</p>
<p>那么,最初的WPF程序的死锁是否可以用.ConfigureAwait(false)解决呢?注意,txtLog是一个文本框,UI控件只能在UI线程访问,所以添加上.ConfigureAwait(false)后会报错:“InvalidOperationException: 调用线程无法访问此对象,因为另一个线程拥有该对象”。那么是否可以改成这样:</p>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">void</span> Button_Click_7(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, RoutedEventArgs e)
      {
            Method1().Wait();
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">async</span><span style="color: rgba(0, 0, 0, 1)"> Task Method1()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">await</span> Task.Delay(<span style="color: rgba(128, 0, 128, 1)">100</span>).ConfigureAwait(<span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">);

            Dispatcher.Invoke(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
                txtLog.AppendText(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">后续代码</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
            });
      }</span></pre>
</div>
<p>依然是死锁。所以,乖乖的用异步事件处理器吧:</p>
<div class="cnblogs_code">
<pre>      <span style="color: rgba(0, 0, 255, 1)">private</span> <strong><span style="color: rgba(0, 0, 255, 1)">async</span> </strong><span style="color: rgba(0, 0, 255, 1)">void</span> Button_Click_7(<span style="color: rgba(0, 0, 255, 1)">object</span><span style="color: rgba(0, 0, 0, 1)"> sender, RoutedEventArgs e)
      {
            </span><strong><span style="color: rgba(0, 0, 255, 1)">await</span></strong><span style="color: rgba(0, 0, 0, 1)"> Method1();
      }

      </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">async</span><span style="color: rgba(0, 0, 0, 1)"> Task Method1()
      {
            </span><span style="color: rgba(0, 0, 255, 1)">await</span> Task.Delay(<span style="color: rgba(128, 0, 128, 1)">100</span><span style="color: rgba(0, 0, 0, 1)">);

            txtLog.AppendText(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">后续代码</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
      }</span></pre>
</div>
<p>上面的代码还说明一个问题:在异步工具方法中,不要写访问UI控件的代码,否则无法规避死锁问题。</p>
<p>&nbsp;</p>
<p><strong>总结</strong></p>
<ol>
<li>
<p class="p1"><span class="s1">死锁会发生在不遵循异步编程规范——在异步方法返回的Task对象上执行Wait()<span class="s2">或<span class="s1">.Result时</span></span></span></p>
</li>
<li>ConfigureAwait(false)指定await后的代码不返回原先的context,可以避免死锁</li>
<li>
<p class="p1"><span class="s1">如果<span class="s2">await<span class="s1">之后的代码不需要返回原先的<span class="s2">context<span class="s1">执行,例如,仅仅是执行<span class="s2">Http<span class="s1">请求,获取和处理数据,那么完全可以加上<span class="s2">ConfigureAwait(false)<span class="s1">。</span></span></span></span></span></span></span></span></span></p>
</li>
<li>如果作为类库的创作者,编写异步方法时,应尽可能的使用ConfigureAwait(false),以保证一旦类库的使用者阻塞异步方法时,不会产生死锁。</li>
<li>在异步类库/工具方法中,应避免加入访问UI控件的代码</li>
</ol>
<p>&nbsp;</p>
<p><strong>附加&nbsp; async/await学习资料</strong></p>
<p>&nbsp;C# Under the Hood: async/await&nbsp; 作者从动手写一个“可等待”的方法开始,进而通过反编译工具分析异步方法生成的的实质代码,揭示了async/await的本质——回调</p>
<p>What happens in an async method&nbsp; msdn编程指南,图示异步方法的执行流程</p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/sdBob/p/12151013.html
頁: [1]
查看完整版本: C# async await 死锁问题总结