同步异步多线程这三者关系,你能给面试官一个满意的回答吗?
前几天一位朋友去面试,面试官问了他同步,异步,多线程之间是什么关系,异步比同步高效在哪?多线程比单线程高效在哪?由于回答的不好,让我帮他捋一下,其实回答这个问题不难,难就难在只对别人说理论,而没有现杀的例子。
一:异步
1. 到底解放了谁?
<1> 从基础的同步说起
要说解放了谁,一定得有几个参与者,举个例子:当你的主线程读取一个应用程序之外的资源时,它有可能是一个文件,又有可能是一个外部服务,当用同步方式读取外部服务时,首先主线程会从用户模式进入到内核模式,在内核模式中windows会将你的请求数据交给对应的网络驱动程序,继后会让这个线程进入休眠状态,当网络驱动程序和外部服务一阵痉挛之后,网络驱动程序会将获取到的结果交给当初休眠的线程,windows唤醒休眠线程继而执行后续的c#代码,画个简图理解一下,不一定全对。
这里就存在着一个非常大的问题,步骤4-步骤7之间,你的主线程一直都是休眠状态,比如在gui编程中,有一个重要的原则就是解放你的ui线程(主线程),所以解决这个问题就迫在眉睫。
<2> 异步方式下的处理方案
说到这里,大家应该知道了异步方式就是为了解放主线程,又可以叫调用线程,没错,接下来看一下同样的场景在异步中如何处理的。
从图中可以看到,步骤三中将thread数据交给网络驱动程序之后,该thread就直接返回不管了,当后续网络驱动程序获取数据后,将数据丢给clr线程池中的io线程再由它触发你的回调函数。
<3> 总结
异步相比同步效率高就高在解放了调用线程,在驱动程序和远程服务roundtrip期间,调用线程还可以执行其他工作,放在gui上就是主线程可以继续响应用户的超敏操作。
由于没有空转的线程,cpu可以得到最满载的运转,更少的线程就有更少的线程栈空间,更少的gc回收时间和上下文切换。
2. 代码演示
还是那句话,光说可不行,你得上一点代码看看,有了上面的理论基础,这里我就模拟爬取下博客园首页的所有文章的用户头像。
<1> 同步代码
public static void main(string[] args) { singlethreaddownloadimages(); console.writeline("主线程继续执行其他的咯~~~"); console.read(); } public static void singlethreaddownloadimages() { using (var client = new httpclient()) { //调用线程 空转等待。。。 var content = client.getstringasync("http://cnblogs.com").result; var html = new htmldocument(); html.loadhtml(content); var imgsrclist = html.documentnode.queryselectorall("img.pfs").select(m => m.attributes["src"].value) .tolist(); console.writeline($"准备下载:{imgsrclist.count}个..."); for (int i = 0; i < imgsrclist.count; i++) { //调用线程 空转等待。。。 var stream = client.getstreamasync(imgsrclist[i]).result; image.fromstream(stream).save($@"c:\2\{i}.jpg"); } } console.writeline("singlethreaddownloadimages 执行结束"); } ------ output ------ 准备下载:19个... singlethreaddownloadimages 执行结束 主线程继续执行其他的咯~~~
<2> 异步代码
public static void main(string[] args) { asyncdownloadimages(); console.writeline("主线程继续执行其他的咯~~~"); console.read(); } public static async void asyncdownloadimages() { using (var client = new httpclient()) { var content = await client.getstringasync("http://cnblogs.com"); var html = new htmldocument(); html.loadhtml(content); var imgsrclist = html.documentnode.queryselectorall("img.pfs").select(m => m.attributes["src"].value) .tolist(); console.writeline($"准备下载:{imgsrclist.count}个..."); for (int i = 0; i < imgsrclist.count; i++) { var stream = await client.getstreamasync(imgsrclist[i]); image.fromstream(stream).save($@"c:\2\{i}.jpg"); } console.writeline("asyncdownloadimages 执行结束"); } } ------ output ------ 主线程继续执行其他的咯~~~ 准备下载:19个... asyncdownloadimages 执行结束
从结果可以看出,异步在获取图片期间,主线程还可以做其他事情,这就是异步最大的特点。
3. windbg 提取是否真为线程池io线程
其实在图2中我口口声声的说是线程池中的io线程回调了你的函数,大家先要明白一个概念,线程池中有两种类别的线程,一个是工作线程,一个是io线程,而工作线程常常就是我们通过代码进行操控,io线程通常由底层clr接管,常常用于处理外部资源的操作,如下threadpool的getmaxthreads方法。
public static void getmaxthreads(out int workerthreads, out int completionportthreads);
有了这个基础,再将 asyncdownloadimages方法修改如下,抓取一下dump文件
var content = await client.getstringasync("http://cnblogs.com"); console.writeline($"已获取到:{content.length}个字符"); console.readline();
~*e !clrstack 查看所有托管线程的调用堆栈
0:000> ~*e !clrstack os thread id: 0x62d8 (13) child sp ip call site 000000da9b1fd1e8 00007ff9fc7bb4f4 [gcframe: 000000da9b1fd1e8] 000000da9b1fd308 00007ff9fc7bb4f4 [gcframe: 000000da9b1fd308] 000000da9b1fd368 00007ff9fc7bb4f4 [helpermethodframe_1obj: 000000da9b1fd368] system.threading.monitor.enter(system.object) 000000da9b1fd460 00007ff9e42f8aff system.io.textreader+synctextreader.readline() 000000da9b1fd4c0 00007ff9e40f0d98 system.console.readline() 000000da9b1fd4f0 00007ff985c81559 consoleapp2.program+d__3.movenext() [c:\dream\csharp\consoleapp1\consoleapp2\program.cs @ 93] 000000da9b1fd690 00007ff9e388cef2 system.threading.executioncontext.runinternal(system.threading.executioncontext, system.threading.contextcallback, system.object, boolean) 000000da9b1fd760 00007ff9e388cd75 system.threading.executioncontext.run(system.threading.executioncontext, system.threading.contextcallback, system.object, boolean) 000000da9b1fd790 00007ff9e38fbe2f system.runtime.compilerservices.asyncmethodbuildercore+movenextrunner.run() 000000da9b1fd7e0 00007ff9e3901343 system.threading.tasks.awaittaskcontinuation.runorscheduleaction(system.action, boolean, system.threading.tasks.task byref) 000000da9b1fd830 00007ff9e3865f40 system.threading.tasks.task.finishcontinuations() 000000da9b1fd8c0 00007ff9e3865a88 system.threading.tasks.task`1[[system.__canon, mscorlib]].trysetresult(system.__canon) 000000da9b1fd900 00007ff9e3865a05 system.threading.tasks.taskcompletionsource`1[[system.__canon, mscorlib]].trysetresult(system.__canon) 000000da9b1fd940 00007ff9c88311a3 system.net.http.httpclient+c__displayclass31_0`1[[system.__canon, mscorlib]].b__1(system.threading.tasks.task`1) 000000da9b1fd990 00007ff9e38f9d47 system.threading.tasks.task.execute() 000000da9b1fd9d0 00007ff9e388cef2 system.threading.executioncontext.runinternal(system.threading.executioncontext, system.threading.contextcallback, system.object, boolean) 000000da9b1fdaa0 00007ff9e388cd75 system.threading.executioncontext.run(system.threading.executioncontext, system.threading.contextcallback, system.object, boolean) 000000da9b1fdad0 00007ff9e38fa001 system.threading.tasks.task.executewiththreadlocal(system.threading.tasks.task byref) 000000da9b1fdb80 00007ff9e38f96e1 system.threading.tasks.task.executeentry(boolean)
!threads 查看编号13的线程类型
0:013> !threads threadcount: 8 unstartedthread: 0 backgroundthread: 5 pendingthread: 0 deadthread: 2 hosted runtime: no lock id osid threadobj state gc mode gc alloc context domain count apt exception 0 1 5754 000001e2be060f80 2a020 preemptive 000001e2bfd19868:000001e2bfd19fd0 000001e2be053bb0 1 mta 6 2 65e0 000001e2be08bd00 2b220 preemptive 0000000000000000:0000000000000000 000001e2be053bb0 0 mta (finalizer) 9 3 25c 000001e2d8435ef0 102a220 preemptive 0000000000000000:0000000000000000 000001e2be053bb0 0 mta (threadpool worker) xxxx 4 0 000001e2d845ea30 1039820 preemptive 0000000000000000:0000000000000000 000001e2be053bb0 0 ukn (threadpool worker) 12 6 23fc 000001e2d8469ea0 202b220 preemptive 000001e2bfd1e188:000001e2bfd1ffd0 000001e2be053bb0 1 mta 13 7 62d8 000001e2d8475e20 a029220 preemptive 000001e2bfd9d588:000001e2bfd9f250 000001e2be053bb0 0 mta (threadpool completion port) xxxx 8 0 000001e2d847a0b0 8039820 preemptive 0000000000000000:0000000000000000 000001e2be053bb0 0 ukn (threadpool completion port) 14 9 6e4 000001e2d847de70 8029220 preemptive 000001e2bfd80d88:000001e2bfd81f10 000001e2be053bb0 0 mta (threadpool completion port)
其中的 13 7 62d8 000001e2d8475e20 a029220 preemptive 000001e2bfd9d588:000001e2bfd9f250 000001e2be053bb0 0 mta (threadpool completion port)
可以明显的看到是 threadpool completion port,没有骗你吧,