C# 彻底搞懂async/await
<h2 id="1-前言">1. 前言</h2><p>Talk is cheap, Show you the code first!</p>
<pre><code class="language-c#">private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
AsyncMethod();
Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
}
private async Task AsyncMethod()
{
var ResultFromTimeConsumingMethod = TimeConsumingMethod();
string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
Console.WriteLine(Result);
//返回值是`Task`的函数可以不用`return`,或者将`Task`改为void
}
//这个函数就是一个耗时函数,可能是`IO`操作,也可能是`cpu`密集型工作。
private Task<string> TimeConsumingMethod()
{
var task = Task.Run(()=> {
Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
return "Hello I am TimeConsumingMethod";
});
return task;
}
</code></pre>
<img src="https://img2022.cnblogs.com/blog/814410/202201/814410-20220128153957077-656401758.png" alt="image" style="zoom: 80%">
<p>我靠,这么复杂!!!竟然有三个函数!!!竟然有那么多行!!!</p>
<p>别着急,慢慢看完,最后的时候你会发现使用<code>async/await</code>真的炒鸡优雅。</p>
<h2 id="2-异步方法的结构">2. 异步方法的结构</h2>
<p>上面是一个的使用<code>async/await</code>的例子(为了方便解说原理我才写的这样复杂的)。<br>
使用<code>async/await</code>能非常简单的创建异步方法,防止耗时操作阻塞当前线程。<br>
使用<code>async/await</code>来构建的异步方法,逻辑上主要有下面三个结构:</p>
<h3 id="21-调用异步方法">2.1 调用异步方法</h3>
<pre><code>private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
AsyncMethod();//这个方法就是异步方法,异步方法的调用与一般方法完全一样
Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
}
</code></pre>
<p>注意:微软建议异步方法的命名是在方法名后添加<code>Aysnc</code>后缀,示例是我为了读起来方便做成了前缀,在真正构建异步方法的时候请注意用后缀。</p>
<p>异步方法的返回类型只能是<code>void</code>、<code>Task</code>、<code>Task<TResult></code>。示例中异步方法的返回值类型是<code>Task</code>。</p>
<p>另外,上面的<code>AsyncMethod()</code>会被编译器提示报警,如下图:<br>
<img src="https://img2018.cnblogs.com/blog/871381/201812/871381-20181228114807481-1116809909.png" alt="img" style="zoom: 80%"><br>
因为是异步方法,所以编译器提示在前面使用<code>await</code>关键字,这个后面再说,为了不引入太多概念导致难以理解暂时就先这么放着。</p>
<h3 id="22-异步方法本体">2.2 异步方法本体</h3>
<pre><code class="language-c#">private async Task AsyncMethod()
{
var ResultFromTimeConsumingMethod = TimeConsumingMethod();
string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
Console.WriteLine(Result);
//返回值是`Task`的函数可以不用`return`,或者将`Task`改为void
}
</code></pre>
<p>用<code>async</code>来修饰一个方法,表明这个方法是异步的,声明的方法的返回类型必须为:<code>void</code>或<code>Task</code>或<code>Task<TResult></code>。方法内部必须含有<code>await</code>修饰的方法,如果方法内部没有<code>await</code>关键字修饰的表达式,哪怕函数被<code>async</code>修饰也只能算作同步方法,执行的时候也是同步执行的。</p>
<p>被<code>await</code>修饰的只能是<code>Task</code>或者<code>Task<TResule></code>类型,通常情况下是一个返回类型是<code>Task/Task<TResult></code>的方法,当然也可以修饰一个<code>Task/Task<TResult></code>变量,<code>await</code>只能出现在已经用<code>async</code>关键字修饰的异步方法中。上面代码中就是修饰了一个变量<code>ResultFromTimeConsumingMethod</code>。</p>
<p>关于被修饰的对象,也就是返回值类型是<code>Task</code>和<code>Task<TResult></code>函数或者<code>Task/Task<TResult></code>类型的变量:如果是被修饰对象的前面用<code>await</code>修饰,那么返回值实际上是<code>void</code>或者<code>TResult</code>(示例中<code>ResultFromTimeConsumingMethod</code>是<code>TimeConsumingMethod()</code>函数的返回值,也就是<code>Task<string></code>类型,当<code>ResultFromTimeConsumingMethod</code>在前面加了<code>await</code>关键字后 <code>await ResultFromTimeConsumingMethod</code>实际上完全等于 <code>ResultFromTimeConsumingMethod.Result</code>)。如果没有<code>await</code>,返回值就是<code>Task</code>或者<code>Task<TResult></code>。</p>
<h3 id="23-耗时函数">2.3 耗时函数</h3>
<pre><code class="language-c#">//这个函数就是一个耗时函数,可能是`IO`密集型操作,也可能是`cpu`密集型工作。
private Task<string> TimeConsumingMethod()
{
var task = Task.Run(()=> {
Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
return "Hello I am TimeConsumingMethod";
});
return task;
}
</code></pre>
<p>这个函数才是真正干活的(为了让逻辑层级更分明,我把这部分专门做成了一个函数,在后面我会精简一下直接放到异步函数中,毕竟活在哪都是干)。</p>
<p>在示例中是一个<code>CPU</code>密集型的工作,我另开一线程让他拼命干了5s。如果是<code>IO</code>密集型工作比如文件读写等可以直接调用<code>.Net</code>提供的类库,对于这些类库底层具体怎么实现的?是用了多线程还是DMA?或者是多线程+DMA?这些问题我没有深究但是从表象看起来和我用<code>Task</code>另开一个线程去做耗时工作是一样的。</p>
<p><code>await</code>只能修饰<code>Task/Task<TResult></code>类型,所以这个耗时函数的返回类型只能是<code>Task/Task<TResult></code>类型。</p>
<p>总结:有了上面三个结构就能完成使用一次异步函数。</p>
<h2 id="3-asyncawait异步函数的原理">3. async/await异步函数的原理</h2>
<p>在开始讲解这两个关键字之前,为了方便,对某些方法做了一些拆解,拆解后的代码块用代号指定:<br>
<img src="https://img2018.cnblogs.com/blog/871381/201812/871381-20181228101854896-997310157.png" style="zoom: 80%"><br>
上图对示例代码做了一些指定具体就是:</p>
<ul>
<li><code>Caller</code>代表调用方函数,在上面的代码中就是<code>button1_Click</code>函数。</li>
<li><code>CalleeAsync</code>代表被调用函数,因为代码中被调用函数是一个异步函数,按照微软建议的命名添加了<code>Async</code>后缀,在上面示例代码中就是<code>AsyncMethod()</code>函数。</li>
<li><code>CallerChild1</code>代表调用方函数<code>button1_Click</code>在调用异步方法<code>CalleeAsync</code>之前的那部分代码。</li>
<li><code>CallerChild2</code>代表调用方函数<code>button1_Click</code>在调用异步方法<code>CalleeAsync</code>之后的那部分代码。</li>
<li><code>CalleeChild1</code>代表被调用方函数<code>AsyncMethod</code>遇到<code>await</code>关键字之前的那部分代码。</li>
<li><code>CalleeChild2</code>代表被调用方函数<code>AsyncMethod</code>遇到<code>await</code>关键字之后的那部分代码。</li>
<li><code>TimeConsumingMethod</code>是指被<code>await</code>修饰的那部分耗时代码(实际上我代码中也是用的这个名字来命名的函数)</li>
</ul>
<h5 id="示例代码的执行流程">示例代码的执行流程</h5>
<img src="https://img2022.cnblogs.com/blog/814410/202201/814410-20220128153957077-656401758.png" alt="image" style="zoom: 50%">
<p>这里涉及到了两个线程,线程<code>ID</code>分别是1和4。</p>
<p><code>Caller</code>函数被调用,先执行<code>CallerChild1</code>代码,这里是同步执行与一般函数一样,然后遇到了异步函数<code>CalleeAsync</code>。</p>
<p>在<code>CalleeAsync</code>函数中有<code>await</code>关键字,<code>await</code>的作用是打分裂点。</p>
<p>编译器会把整个函数<code>CalleeAsync</code>从这里分裂成两个函数。<code>await</code>关键字之前的代码作为一个函数(按照我上面定义的指代,下文中就叫这部分代码<code>CalleeChild1</code>)<code>await</code>关键字之后的代码作为一个函数<code>CalleeChild2</code>。</p>
<p><code>CalleeChild1</code>在调用方线程执行(在示例中就是主线程<code>Thread1</code>),执行到<code>await</code>关键字之后,另开一个线程耗时工作在<code>Thread4</code>中执行,然后立即返回。这时调用方会继续执行下面的代码<code>CallerChild2</code>(注意是<code>Caller</code>不是<code>Callee</code>)。</p>
<p>在<code>CallerChild2</code>被执行期间,<code>TimeConsumingMethod</code>也在异步执行(可能是在别的线程也可能是<code>CPU</code>不参与操作直接<code>DMA</code>的<code>IO</code>操作)。</p>
<p>当<code>TimeConsumingMethod</code>执行结束后,<code>CalleeChild2</code>也就具备了执行条件,而这个时候<code>CallerChild2</code>可能执行完了也可能没有,由于<code>CallerChild2</code>与<code>CalleeChild2</code>都会在<code>Caller</code>的线程执行,这里就会有冲突应该先执行谁,编译器会在合适的时候在<code>Caller</code>的线程执行这部分代码。示意图如下:<br>
<img src="https://img2022.cnblogs.com/blog/814410/202201/814410-20220128161338118-969071404.png" style="zoom: 50%"></p>
<p>请注意,<code>CalleeChild2</code>在上图中并没有画任何箭头,因为这部分代码的执行是由编译器决定的,暂时无法具体描述是什么时候执行。</p>
<p><strong>总结</strong>:整个流程下来,除了<code>TimeConsumingMethod</code>函数是在<code>Thread4</code>中执行的,剩余代码都是在主线程<code>Thread1</code>中执行的。也就是说异步方法运行在当前同步上下文中,只有激活的时候才占用当前线程的时间,异步模型采用时间片轮转来实现。你也许会说,明明新加了一个<code>Thread4</code>线程怎么能说是运行在当前的线程中呢?这里说的异步方法运行在当前线程上的意思是由<code>CalleeAsync</code>分裂出来的<code>CalleeChild1</code>和<code>CalleeChild2</code>的确是运行在Thread1上的。</p>
<h2 id="4-带返回值的异步函数">4. 带返回值的异步函数</h2>
<p>之前的示例代码中异步函数是没有返回值的,作为理解原理足够了,但是在实际应用场景中,带返回值的应用才是最常用的。那么,上代码:</p>
<pre><code class="language-c#">private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
var ResultTask= AsyncMethod();
Console.WriteLine(ResultTask.Result);
Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
}
private async Task<string> AsyncMethod()
{
var ResultFromTimeConsumingMethod = TimeConsumingMethod();
string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
Console.WriteLine(Result);
return Result;
}
//这个函数就是一个耗时函数,可能是`IO`操作,也可能是`cpu`密集型工作。
private Task<string> TimeConsumingMethod()
{
var task = Task.Run(()=> {
Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
return "Hello I am TimeConsumingMethod";
});
return task;
}
</code></pre>
<img src="https://img2022.cnblogs.com/blog/814410/202201/814410-20220128163245051-385283599.png" style="zoom: 67%">
<p>主要更改的地方在这里:</p>
<img src="https://img2018.cnblogs.com/blog/871381/201812/871381-20181228115825237-311861378.png" alt="img">
按理说没错吧?然而,这代码一旦执行就会卡死。
<h3 id="41-死锁">4.1 死锁</h3>
<p>是的,死锁。分析一下为什么:<br>
<img src="https://img2018.cnblogs.com/blog/871381/201812/871381-20181228120141259-1186635157.png" alt="img" loading="lazy"></p>
<p>按照之前我划定的代码块指定,在添加了新代码后<code>CallerChild2</code>与<code>CalleeChild2</code>的划分如上图。</p>
<p>这两部分代码块都是在同一个线程上执行的,也就是主线程<code>Thread1</code>,而且通常情况下<code>CallerChild2</code>是会早于<code>CalleeChild2</code>执行的(毕竟<code>CalleeChild2</code>得在耗时代码块执行之后执行)。</p>
<p><code>Console.WriteLine(ResultTask.Result);</code>(<code>CallerChild2</code>)其实是在请求<code>CalleeChild2</code>的执行结果,此时明显<code>CalleeChild2</code>还没有结束没有<code>return</code>任何结果,那<code>Console.WriteLine(ResultTask.Result);</code>就只能阻塞<code>Thread1</code>等待,直到<code>CalleeChild2</code>有结果。</p>
<p>然而问题就在这,<code>CalleeChild2</code>也是在<code>Thread1</code>上执行的,此时<code>CallerChild2</code>一直占用<code>Thread1</code>等待<code>CalleeChild2</code>的结果,耗时程序结束后轮到CalleeChild2执行的时候<code>CalleeChild2</code>又因<code>Thread1</code>被<code>CallerChild2</code>占用而抢不到线程,永远无法<code>return</code>,那么<code>CallerChild2</code>就会永远等下去,这就造成了死锁。</p>
<h3 id="42-解决方法">4.2 解决方法</h3>
<p>解决办法有两种一个是把<code>Console.WriteLine(ResultTask.Result);</code>放到一个新开线程中等待(个人觉得这方法有点麻烦,毕竟要新开线程),还有一个方法是把<code>Caller</code>也做成异步方法:<br>
<img src="https://img2018.cnblogs.com/blog/871381/201812/871381-20181228121129970-1749904869.png" alt="img" loading="lazy"><br>
<code>ResultTask.Result</code>变成了<code>ResultTask </code>的原因上面也说了,<code>await</code>修饰的<code>Task/Task<TResult></code>得到的是<code>TResult</code>。</p>
<p>之所以这样就能解决问题是因为嵌套了两个异步方法,现在的<code>Caller</code>也成了一个异步方法,当<code>Caller</code>执行到<code>await</code>后直接返回了(<code>await</code>拆分方法成两部分),<code>CalleeChild2</code>执行之后才轮到<code>Caller</code>中<code>await</code>后面的代码块(<code>Console.WriteLine(ResultTask.Result);</code>)。</p>
<p>另外,把<code>Caller</code>做成异步的方法也解决了一开始的那个警告,还记得么?<br>
<img src="https://img2018.cnblogs.com/blog/871381/201812/871381-20181228114807481-1116809909.png" alt="img" style="zoom: 80%"></p>
<h2 id="5-质疑">5. 质疑</h2>
<p>到现在,你可能会说:使用<code>async/await</code>不比直接用<code>Task.Run()</code>来的简单啊?比如我用<code>Task</code>的<code>TaskContinueWith</code>方法也能实现:</p>
<pre><code class="language-c#">private void button1_Click(object sender, EventArgs e)
{
var ResultTask = Task.Run(()=> {
Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
return "Hello I am TimeConsumingMethod";
});
ResultTask.ContinueWith(OnDoSomthingIsComplete);
}
private void OnDoSomthingIsComplete(Task<string> t)
{
Action action = () => { textBox1.Text = t.Result;};
textBox1.Invoke(action);
Console.WriteLine("Continue Thread ID :" + Thread.CurrentThread.ManagedThreadId);
}
</code></pre>
<p>是的,上面的代码也能实现。但是,<code>async/await</code>的优雅的打开方式是这样的:</p>
<pre><code class="language-c#">private async void button1_Click(object sender, EventArgs e)
{
var t = Task.Run(() => {
Thread.Sleep(5000);
return "Hello I am TimeConsumingMethod";
});
textBox1.Text = await t;
}
</code></pre>
<p>看到没,惊不惊喜,意不意外,寥寥几行就搞定了,不用再多写那么多函数,使用起来也很灵活。最让人头疼的跨线程修改控件的问题完美解决了,再也不用使用<code>Invoke</code>了,因为修改控件的操作压根就是在原来的线程上做的,还能不阻塞<code>UI</code>。</p>
<blockquote>
<p><strong>注:感谢17楼和23楼同学对死锁的解释,并给出了其它解决办法,你们是对的!同时也感谢其他同学的互动参与,通过交流相互提高才是我们想要的!</strong></p>
</blockquote><br><br>
来源:https://www.cnblogs.com/zhaoshujie/p/11192036.html
頁:
[1]