31

同步异步多线程这三者关系,你能给面试官一个满意的回答吗?

 4 years ago
source link: http://www.cnblogs.com/huangxincheng/p/12739752.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前几天一位朋友去面试,面试官问了他同步,异步,多线程之间是什么关系,异步比同步高效在哪?多线程比单线程高效在哪?由于回答的不好,让我帮他捋一下,其实回答这个问题不难,难就难在只对别人说理论,而没有现杀的例子。

一:异步

1. 到底解放了谁?

<1> 从基础的同步说起

要说解放了谁,一定得有几个参与者,举个例子:当你的主线程读取一个应用程序之外的资源时,它有可能是一个文件,又有可能是一个外部服务,当用同步方式读取外部服务时,首先主线程会从用户模式进入到内核模式,在内核模式中windows会将你的请求数据交给对应的网络驱动程序,继后会让这个线程进入休眠状态,当网络驱动程序和外部服务一阵痉挛之后,网络驱动程序会将获取到的结果交给当初休眠的线程,windows唤醒休眠线程继而执行后续的C#代码,画个简图理解一下,不一定全对。

vemq2au.png!web

这里就存在着一个非常大的问题,步骤4-步骤7之间,你的主线程一直都是休眠状态,比如在GUI编程中,有一个重要的原则就是解放你的UI线程(主线程),所以解决这个问题就迫在眉睫。

<2> 异步方式下的处理方案

说到这里,大家应该知道了异步方式就是为了解放主线程,又可以叫调用线程,没错,接下来看一下同样的场景在异步中如何处理的。

R7nmuan.png!web

从图中可以看到,步骤三中将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,没有骗你吧,:smile:。

二:多线程

相比单线程,多线程用更多的CPU和更多的线程资源换取更快的计算时间,是一种经典的空间换时间策略,代码就不上了,相信多线程大家都快用烂了。

三:总结

1. 多线程比单线程高效的原因就是利用了CPU的多核计算把一个大的任务分而治之从而加速任务计算。

2. 异步比同步高效的原因是前者释放了调用线程,让调用线程可以做更多的事情而不至于被windows强制休眠浪费线程资源。

言简意赅吧~, 好了,本篇就说到这里,希望对你有帮助。

如您有更多问题与我互动,扫描下方进来吧~

fmUvaqM.jpg!webj6z2Ibr.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK