同步异步-多线程梳理
毕业好多年,从一开始面试起就很多面试官喜欢问这个问题。其实也是说的不是很清晰,似懂非懂,今天我就把这个好好的梳理一下,形成一个知识节点。我在网上也搜索了一下,关于同步异步、多线程有很多实例,我今天就是把我的理解进行口语化进行描述,让大家容易理解,也是让自己记忆更深刻。所以一下概念都是自己的理解,如需要官方解释请左转gg。
概念
1、进程--线程--多线程
- 进程:程序在机器上运行时全部占用计算机资源、虚拟的数据的集合。
- 线程:进程在相应操作时一个执行的动作或者一个任务,同时也包含计算机的资源信息。
- 句柄:其实是一个long数字,是程序所需要资源的ID,可以理解为页面的窗体、按钮等信息
- 多线程:一个进程中多个线程同时运行。
估计说了这些概念,还是不容易理解。用计算机“”任务管理器“”大家就容易明白。
线程池
顾名思义,就是管理线程的程序,类似一个线程池子。线程池主要有两个好处:
- 避免线程创建和销毁的开销。
- 自动缩放:可以按需增减线程的数量。
总之,Windows系统自带了线程池的功能,通常情况下,你不可能有更好的实现。所以只需了解如何使用。
Windows的线程池有两种,分别是非托管线程池和托管线程池(即.NET线程池)
2、并行和并发
不知道有没有想过为什么计算机可以多线程呢?
- 多个CPU的核可以并行工作。
比如我的电脑就是4个内核8个逻辑处理器,这里8个就是指的8个模拟核。如果8个核在同一进程中同时进行计算,则就是多核工作。
并行:多核之间同时运行称为并行。
- 因为一个CPU本身的计算量就很庞大,为了提高利用率,CPU都是分片处理的。比如我的电脑边写文章,边听歌。一个1S的处理能力分成1000份,则第一份去处理word,第二份处理播放软件,第三份又处理word,第四份处理播放软件……依次不断进行。因为时间分片比较短,人根本不可能察觉。所以电脑就给人的感官就是既可以写word又可以听歌,都是同时进行的。
从长时间段来看:感觉就是多个任务在并发执行。
从微时间段来看:一个物理CPU同一时间只能处理一个任务。
并发:一个CPU分片运行的称为并发。
3、同步和异步
同步方法:调用在程序继续执行之前需要等待同步方法执行完毕返回结果
异步方法:在被调用之后立即返回以便程序在被调用方法完成其任务的同时执行其它操作。
下面直接撸代码,这样直接又可观。
同步代码:
private void Button_SyncClick(object sender, RoutedEventArgs e) { Console.WriteLine($"===== 同步调用 SyncInvokeTest Start,ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); for (int i = 0; i < 5; i++) { Dothings(i.ToString()); } Console.WriteLine($"===== 同步调用 SyncInvokeTest End, ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); }
异步代码:
private void Button_AsyncClick(object sender, RoutedEventArgs e) { Console.WriteLine($"===== 异步调用 AsyncInvokeTest start,ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); for (int i = 0; i < 5; i++) { Action<string> action = this.Dothings; action.BeginInvoke("", null, null); } Console.WriteLine($"===== 异步调用 AsyncInvokeTest End, ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); }
好似操作:
private void Dothings(string name) { Console.WriteLine($"******* 方法调用 Dothings start,ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); Thread.Sleep(2000); //模拟该方法运行2秒 Console.WriteLine($"******* 方法调用 Dothings end, ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")} {name}"); }
分别执行结果如下:
同步:
异步:
已上图可见,同步是在一个线程里,依次完成的。而异步是多个线程,主线程(001)直接就执行了,后续分配5个子线程依次完成无需等待。
4.NET多线程及异步编程模型
- .NET 3.5及之前
只有Thread
,ThreadPool
等。直接操作线程。 - .NET 4
引入了Task
。 - .NET 4.5.1 (C# 5.0)
引入了TAP(基于Task的异步模式),引入了async/await
。
关于Thread实例
private void Button_ThreadClick(object sender, RoutedEventArgs e) { Console.WriteLine($"===== 主线程开始 Button_ThreadClick Start,ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); //实例化Thread,默认创建前台线程 Thread t1 = new Thread(DoRun1); t1.Start(); //可以通过修改Thread的IsBackground,将其变为后台线程 Thread t2 = new Thread(DoRun2) { IsBackground = true }; t2.Start(); Console.WriteLine($"===== 主线程结束 Button_ThreadClick end, ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); } static void DoRun1() { Console.WriteLine($"===== 前台调用 DoRun1 Start,ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); Thread.Sleep(1000); Console.WriteLine($"===== 前台调用 DoRun1 end, ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); } static void DoRun2() { Console.WriteLine($"===== 后台线程调用 DoRun1 Start,ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); Thread.Sleep(2000); Console.WriteLine($"===== 后台线程调用 DoRun1 Start,ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); }
结果如下:
运行上面的代码,如果是用的控制台的话DoRun2方法的打印信息“后台线程调用”将不会被显示出来,因为我这里是wpf的,比较懒不想写了,所以看不出效果,不信的可以试一下。
async/await 异步编程
private void Button_Click(object sender, RoutedEventArgs e) { Console.WriteLine($"******* 调用 async click start,ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); Task<int> task = AccessAsync(); Console.WriteLine("######## 按钮方法做其他事情1"); Console.WriteLine("######## 按钮方法做其他事情2"); Console.WriteLine("Task返回的值" + task.Result); resultsTextBox.Text += "Task返回的值" + task.Result+"\r\n"; Console.WriteLine($"******* 调用 async click end, ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); } private async Task<int> AccessAsync() { Console.WriteLine($"******* 调用 AccessTheWebAsync start,ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); DoIndependentWork("method1"); string str = await GetStringAsync().ConfigureAwait(false); //DoIndependentWork("method2"); Console.WriteLine($"******* 调用 AccessTheWebAsync end,ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")}"); return str.Length; } private Task<string> GetStringAsync() { return Task<string>.Run(() => { Thread.Sleep(2000); return "Hello"; }); } private void DoIndependentWork(string name) { Console.WriteLine($"******* 调用 DoIndependentWork start,ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")} {name}"); resultsTextBox.Text += "\r\nWorking . . . . . . .\r\n"; Console.WriteLine($"******* 调用 DoIndependentWork end ,ID: {Thread.CurrentThread.ManagedThreadId.ToString("000")} {DateTime.Now.ToString("HH:mm:ss fff")} {name}"); }
显示如下:
根据结果来看,有几点要注意的:
- task.Result会在自己调用的地方进行等待,等结果出来之后才执行后面的方法。
- 虽然DoIndependentWork方法很早就执行了,而且打印记录也出来了。“”Working……“的显示是等线程返回结果之后才运行的,所以这里两行语句显示都是同时显示的。
-
string str = await GetStringAsync().ConfigureAwait(false); 这里一定要加上,因为后面有页面信息的展示,不然就会与页面线程冲突,程序不响应。