C#异步的世界【下】
接上篇:《》 上篇主要分析了asyncawait之前的一些异步模式,今天说异步的主要是指C#5的asyncawait异步。在此为了方便的表述,我们称asyncawait之前的异步为”,asyncawait为”。 新异步的使用只能说新异步的使用太简单() 方法加上async修饰符,然后使用await关键字执行异步方法,即可。对就是如此简单。像使用同步方法逻辑一样使用异步。 Task<> num1 = GetNumber( num2 = task =
num3 = num1 + num2 +
新异步的优势在此之前已经有了多种异步模式,为什么还要引入和学习新的asyncawait异步呢?当然它肯定是有其独特的优势。 我们分两个方面来分析:WinForm、WPF等单线程UI程序和Web后台服务程序。 对于WinForm、WPF等单线程UI程序代码1(旧异步) button1_Click( request = WebRequest.Create( AsyncCallback(t =>
label1.Invoke((Action)(() => { label1.Text = ; }));
}),
代码2(同步) button3_Click(= htmlStr = http.GetStringAsync(
label1.Text = ;
}
代码3(新异步) button2_Click(= htmlStr = http.GetStringAsync(
label1.Text = ;
}
新异步的优势:
?是的,说得再多还不如看看实际效果图来得实际: 【思考】:旧的异步模式是开启了一个新的线程去执行,不会阻塞UI线程。这点很好理解。可是,新的异步看上去和同步区别不大,为什么也不会阻塞界面呢? 【原因】:新异步,在执行await表达式前都是使用UI线程,await表达式后会启用新的线程去执行异步,直到异步执行完成并返回结果,然后再回到UI线程()。所以,await是没有阻塞UI线程的,也就不会造成界面的假死。 【注意】:我们在演示同步代码的时候使用了Result。然,在UI单线程程序中使用Result来使异步代码当同步代码使用是一件很危险的事()。至于具体原因稍候再分析()。 对于Web后台服务程序也许对于后台程序的影响没有单线程程序那么直观,但其价值也是非常大的。且很多人对新异步存在误解。 【误解】:新异步可以提升Web程序的性能。 【正解】:异步不会提升单次请求结果的时间,但是可以提高Web程序的吞吐量。 1、为什么不会提升单次请求结果的时间? 其实我们从上面示例代码()也可以看出。 ? 2、为什么可以提高Web程序的吞吐量? 那什么是吞吐量呢,也就是本来只能十个人同时访问的网站现在可以二十个人同时访问了。也就是常说的并发量。 还是用上面的代码来解释。[代码2] 阻塞了UI线程等待请求结果,所以UI线程被占用,而[代码3]使用了新的线程请求,所以UI线程没有被占用,而可以继续响应UI界面。 那问题来了,我们的Web程序天生就是多线程的,且web线程都是跑的线程池线程(),而线程池线程可使用线程数量是一定的,尽管可以设置,但它还是会在一定范围内。如此一来,我们web线程是珍贵的(),不能滥用。用完了,那么其他用户请求的时候就无法处理直接503了。 那什么算是滥用呢?比如:文件读取、URL请求、数据库访问等IO请求。如果用web线程来做这个耗时的IO操作那么就会阻塞web线程,而web线程阻塞得多了web线程池线程就不够用了。也就达到了web程序最大访问数。 此时我们的新异步横空出世,解放了那些原本处理IO请求而阻塞的web线程()。通过异步方式使用相对廉价的线程()来处理IO操作,这样web线程池线程就可以解放出来处理更多的请求了。 不信?下面我们来测试下: 【测试步骤】: 1、新建一个web api项目? 2、新建一个数据访问类,分别提供同步、异步方法()
beginInfo = (HttpClient http = ).Wait();
beginInfo +
</span><span style="color: #808080;">///</span> <span style="color: #808080;"><summary></span>
<span style="color: #808080;">///</span><span style="color: #008000;"> 异步方法获取数据
</span><span style="color: #808080;">///</span> <span style="color: #808080;"></summary></span>
<span style="color: #808080;">///</span> <span style="color: #808080;"><returns></returns></span>
<span style="color: #0000ff;">public</span> <span style="color: #0000ff;">async</span> Task<<span style="color: #0000ff;">string</span>><span style="color: #000000;"> GetDataAsync()
{
</span><span style="color: #0000ff;">var</span> beginInfo =<span style="color: #000000;"> GetBeginThreadInfo();
</span><span style="color: #0000ff;">using</span> (HttpClient http = <span style="color: #0000ff;">new</span><span style="color: #000000;"> HttpClient())
{
</span><span style="color: #0000ff;">await</span> http.GetStringAsync(<span style="color: #800000;">"</span><span style="color: #800000;">https://github.com/</span><span style="color: #800000;">"</span>);<span style="color: #008000;">//</span><span style="color: #008000;">注意:这里是异步等待</span>
<span style="color: #000000;"> } }
}
Task<> Get(= : :
同步链接:http://localhost:803/api/Home?str=同步处理? 异步链接:http://localhost:803/api/Home?str=异步处理 5、接着上面的winform程序里面测试请求:() button6_Click(= = =><span style="color: #0000ff;">private <span style="color: #0000ff;">void button5_Click(<span style="color: #0000ff;">object<span style="color: #000000;"> sender,EventArgs e)
{ textBox1.Text = <span style="color: #800000;">""<span style="color: #000000;">; label1.Text = <span style="color: #800000;">""<span style="color: #000000;">; Task.Run(() =><span style="color: #000000;"> { TestResultUrl(<span style="color: #800000;">"<span style="color: #800000;">http://localhost:803/api/Home?str=异步处理<span style="color: #800000;">"<span style="color: #000000;">); }); } <span style="color: #0000ff;">public <span style="color: #0000ff;">void TestResultUrl(<span style="color: #0000ff;">string<span style="color: #000000;"> url)
}
7、启动winform程序,点击“”: 8、重复6,然后重新启动winform程序点击“” 看到这些数据有什么感想? 数据和我们前面的【正解】完全吻合。仔细观察,每个单次请求用时基本上相差不大。 但是"同步实现"最高投入web线程数是10,而“异步实现”最高投入web线程数是3。 也就是说“异步实现”使用更少的web线程完成了同样的请求数量,如此一来我们就有更多剩余的web线程去处理更多用户发起的请求。 接着我们还发现同步实现请求前后的线程ID是一致的,而异步实现前后线程ID一致。再次证明执行await异步前释放了主线程。 【结论】:
【图解】: Result的死锁陷阱我们在分析UI单线程程序的时候说过,要慎用异步的Result属性。下面我们来分析: button4_Click(= GetUlrString(<span style="color: #0000ff;">public <span style="color: #0000ff;">async Task<<span style="color: #0000ff;">string> GetUlrString(<span style="color: #0000ff;">string<span style="color: #000000;"> url)
{ <span style="color: #0000ff;">using (HttpClient http = <span style="color: #0000ff;">new<span style="color: #000000;"> HttpClient()) { <span style="color: #0000ff;">return <span style="color: #0000ff;">await<span style="color: #000000;"> http.GetStringAsync(url); } } 代码?).Result?的Result属性会阻塞UI线程,而执行到GetUlrString方法的 await异步的时候又要释放UI线程。此时矛盾就来了,由于线程资源的抢占导致死锁。 此等问题在Web服务程序里面一样存在。(不一定会回到原来的主线程,而UI程序一定会回到原来的UI线程) 我们前面说过,.net为什么会这么智能的自动释放主线程然后等待异步执行完毕后又回到主线程是因为的功劳。 但这里有个例外,那就是控制台程序里面是没有的。所以这段代码放在控制台里面运行是没有问题的。 Main(<span style="color: #0000ff;">public <span style="color: #0000ff;">async <span style="color: #0000ff;">static Task<<span style="color: #0000ff;">string> GetUlrString(<span style="color: #0000ff;">string<span style="color: #000000;"> url)
{ <span style="color: #0000ff;">using (HttpClient http = <span style="color: #0000ff;">new<span style="color: #000000;"> HttpClient()) { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); <span style="color: #0000ff;">return <span style="color: #0000ff;">await<span style="color: #000000;"> http.GetStringAsync(url); } } 打印出来的都是同一个线程ID 使用AsyncHelper在同步代码里面调用异步但可是,可但是,我们必须在同步方法里面执行异步怎办?办法肯定是有的 我们首先定义一个AsyncHelper静态类: TaskFactory _myTaskFactory =
</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> TResult RunSync<TResult>(Func<Task<TResult>><span style="color: #000000;"> func)
{
</span><span style="color: #0000ff;">return</span><span style="color: #000000;"> _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
}
</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">void</span> RunSync(Func<Task><span style="color: #000000;"> func)
{
_myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
}
} 然后调用异步: button7_Click(= AsyncHelper.RunSync(() => GetUlrString(
这样就不会死锁了。 ConfigureAwait除了AsyncHelper我们还可以使用Task的ConfigureAwait方法来避免死锁 button7_Click(= GetUlrString(<span style="color: #0000ff;">public <span style="color: #0000ff;">async Task<<span style="color: #0000ff;">string> GetUlrString(<span style="color: #0000ff;">string<span style="color: #000000;"> url)
{ <span style="color: #0000ff;">using (HttpClient http = <span style="color: #0000ff;">new<span style="color: #000000;"> HttpClient()) { <span style="color: #0000ff;">return <span style="color: #0000ff;">await http.GetStringAsync(url).<span style="color: #ff0000;">ConfigureAwait(<span style="color: #0000ff;">false<span style="color: #000000;">); } } ConfigureAwait的作用:使的不需要恢复到主线程()。 异常处理关于新异步里面抛出异常的正确姿势。我们先来看下面一段代码: button8_Click(<> task = GetUlrStringErr();
textBox1.Text = <span style="color: #0000ff;">public <span style="color: #0000ff;">async Task<<span style="color: #0000ff;">string> GetUlrStringErr(<span style="color: #0000ff;">string<span style="color: #000000;"> url)
{ <span style="color: #0000ff;">if (<span style="color: #0000ff;">string<span style="color: #000000;">.IsNullOrWhiteSpace(url)) { <span style="color: #0000ff;">throw <span style="color: #0000ff;">new Exception(<span style="color: #800000;">"<span style="color: #800000;">url不能为空<span style="color: #800000;">"<span style="color: #000000;">); } <span style="color: #0000ff;">using (HttpClient http = <span style="color: #0000ff;">new<span style="color: #000000;"> HttpClient()) { <span style="color: #0000ff;">return <span style="color: #0000ff;">await<span style="color: #000000;"> http.GetStringAsync(url); } } 调试执行执行流程: 在执行完118行的时候竟然没有把异常抛出来?这不是逆天了吗。非得在等待await执行的时候才报错,显然119行的逻辑执行是没有什么意义的。让我们把异常提前抛出: 提取一个方法来做验证,这样就能及时的抛出异常了。有朋友会说这样的太坑爹了吧,一个验证还非得另外写个方法。接下来我们提供一个没有这么坑爹的方式: 在异步函数里面用匿名异步函数进行包装,同样可以实现及时验证。 感觉也不比前种方式好多少...可是能怎么办呢。 异步的实现上面简单分析了新异步能力和属性。接下来让我们继续揭秘异步的本质,神秘的外套下面究竟是怎么实现的。 首先我们编写一个用来反编译的示例: Task<> GetUrlStringAsync(HttpClient http, url,
为了方便阅读,我们把编译器自动命名的类型重命名。 ??方法变成了如此模样: Task<> GetUrlStringAsync(HttpClient http,= = ==== AsyncTaskMethodBuilder<>= -
方法签名完全一致,只是里面的内容变成了一个状态机??的调用。此状态机就是编译器自动创建的。下面来看看神秘的状态机是什么鬼: AsyncTaskMethodBuilder<> TaskAwaiter<>
</span><span style="color: #0000ff;">private</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> MoveNext()
{
</span><span style="color: #0000ff;">string</span><span style="color: #000000;"> str;
</span><span style="color: #0000ff;">int</span> num = <span style="color: #0000ff;">this</span><span style="color: #000000;">._state;
</span><span style="color: #0000ff;">try</span><span style="color: #000000;">
{
TaskAwaiter awaiter;
MyAsyncTest.GetUrlStringAsyncdStateMachine d__;
</span><span style="color: #0000ff;">string</span><span style="color: #000000;"> str2;
</span><span style="color: #0000ff;">switch</span><span style="color: #000000;"> (num)
{
</span><span style="color: #0000ff;">case</span> <span style="color: #800080;">0</span><span style="color: #000000;">:
</span><span style="color: #0000ff;">break</span><span style="color: #000000;">;
</span><span style="color: #0000ff;">case</span> <span style="color: #800080;">1</span><span style="color: #000000;">:
</span><span style="color: #0000ff;">goto</span><span style="color: #000000;"> Label_00CD;
</span><span style="color: #0000ff;">default</span><span style="color: #000000;">:<span style="color: #ff0000;"> //这里是异步方法?await Task.Delay(time);的具体实现</span>
awaiter </span>= Task.Delay(<span style="color: #0000ff;">this</span><span style="color: #000000;">.time).GetAwaiter();
</span><span style="color: #0000ff;">if</span><span style="color: #000000;"> (awaiter.IsCompleted)
{
</span><span style="color: #0000ff;">goto</span><span style="color: #000000;"> Label_0077;
}
</span><span style="color: #0000ff;">this</span>._state = num = <span style="color: #800080;">0</span><span style="color: #000000;">;
</span><span style="color: #0000ff;">this</span>.taskAwaiter1 =<span style="color: #000000;"> awaiter;
d__ </span>= <span style="color: #0000ff;">this</span><span style="color: #000000;">;
</span><span style="color: #0000ff;">this</span>._builder.AwaitUnsafeOnCompleted<TaskAwaiter,MyAsyncTest.GetUrlStringAsyncdStateMachine>(<span style="color: #0000ff;">ref</span> awaiter,<span style="color: #0000ff;">ref</span><span style="color: #000000;"> d__);
</span><span style="color: #0000ff;">return</span><span style="color: #000000;">;
}
awaiter </span>= <span style="color: #0000ff;">this</span><span style="color: #000000;">.taskAwaiter1;
</span><span style="color: #0000ff;">this</span>.taskAwaiter1 = <span style="color: #0000ff;">new</span><span style="color: #000000;"> TaskAwaiter();
</span><span style="color: #0000ff;">this</span>._state = num = -<span style="color: #800080;">1</span><span style="color: #000000;">;
Label_0077:
awaiter.GetResult();
awaiter </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> TaskAwaiter();<span style="color: #ff0000;"> //这里是异步方法await http.GetStringAsync(url);的具体实现</span>
TaskAwaiter</span><<span style="color: #0000ff;">string</span>> awaiter2 = <span style="color: #0000ff;">this</span>.http.GetStringAsync(<span style="color: #0000ff;">this</span><span style="color: #000000;">.url).GetAwaiter();
</span><span style="color: #0000ff;">if</span><span style="color: #000000;"> (awaiter2.IsCompleted)
{
</span><span style="color: #0000ff;">goto</span><span style="color: #000000;"> Label_00EA;
}
</span><span style="color: #0000ff;">this</span>._state = num = <span style="color: #800080;">1</span><span style="color: #000000;">;
</span><span style="color: #0000ff;">this</span>.taskAwaiter2 =<span style="color: #000000;"> awaiter2;
d__ </span>= <span style="color: #0000ff;">this</span><span style="color: #000000;">;
</span><span style="color: #0000ff;">this</span>._builder.AwaitUnsafeOnCompleted<TaskAwaiter<<span style="color: #0000ff;">string</span>>,MyAsyncTest.GetUrlStringAsyncdStateMachine>(<span style="color: #0000ff;">ref</span> awaiter2,<span style="color: #0000ff;">ref</span><span style="color: #000000;"> d__);
</span><span style="color: #0000ff;">return</span><span style="color: #000000;">;
Label_00CD:
awaiter2 </span>= <span style="color: #0000ff;">this</span><span style="color: #000000;">.taskAwaiter2;
</span><span style="color: #0000ff;">this</span>.taskAwaiter2 = <span style="color: #0000ff;">new</span> TaskAwaiter<<span style="color: #0000ff;">string</span>><span style="color: #000000;">();
</span><span style="color: #0000ff;">this</span>._state = num = -<span style="color: #800080;">1</span><span style="color: #000000;">;
Label_00EA:
str2 </span>=<span style="color: #000000;"> awaiter2.GetResult();
awaiter2 </span>= <span style="color: #0000ff;">new</span> TaskAwaiter<<span style="color: #0000ff;">string</span>><span style="color: #000000;">();
</span><span style="color: #0000ff;">this</span>._str1 =<span style="color: #000000;"> str2;
str </span>= <span style="color: #0000ff;">this</span><span style="color: #000000;">._str1;
}
</span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (Exception exception)
{
</span><span style="color: #0000ff;">this</span>._state = -<span style="color: #800080;">2</span><span style="color: #000000;">;
</span><span style="color: #0000ff;">this</span><span style="color: #000000;">._builder.SetException(exception);
</span><span style="color: #0000ff;">return</span><span style="color: #000000;">;
}
</span><span style="color: #0000ff;">this</span>._state = -<span style="color: #800080;">2</span><span style="color: #000000;">;
</span><span style="color: #0000ff;">this</span><span style="color: #000000;">._builder.SetResult(str);
}
[DebuggerHidden]
</span><span style="color: #0000ff;">private</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> SetStateMachine(IAsyncStateMachine stateMachine)
{
}
} 明显多个异步等待执行的时候就是在不断调用状态机中的MoveNext()方法。经验来至我们之前分析过的,不过今天的这个明显复杂度要高于以前的那个。猜测是如此,我们还是来验证下事实: 在起始方法??第一次启动状态机? stateMachine);? ?确实是调用了??。因为_state的初始值是-1,所以执行到了下面的位置: 绕了一圈又回到了??。由此,我们可以现象成多个异步调用就是在不断执行MoveNext直到结束。 说了这么久有什么意思呢,似乎忘记了我们的目的是要通过之前编写的测试代码来分析异步的执行逻辑的。 再次贴出之前的测试代码,以免忘记了。 反编译后代码执行逻辑图: 当然这只是可能性较大的执行流程,但也有??为??的情况。其他可能的留着大家自己去琢磨吧。? 本文已同步至索引目录:《》 本文demo: 【推荐】 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |