线程相关概念
在学习多线程之前,先来了解下几个与多线程相关的概念。
进程:进程是计算机的概念,程序在服务器运行时占据全部计算资源的总和,一个应用程序运行起来就是一个进程,打开windows的任务管理器,如下图
线程:线程也是计算机的概念,线程是进程的最小单位,也是程序在响应操作系统时的最小单位,一个进程至少由一个线程(主线程)构成。线程和进程一样也会占据一定的CPU、内存、网络、硬盘IO等。一个线程隶属于某个进程,进程销毁,线程也随之销毁。
句柄:是一个long类型的数字,是操作系统用来标识应用程序的,有点主键或者身份证号码的意思。
多线程:一个进程或者说一个应用程序有多个线程在运行参与计算。
C#里面的多线程
Thread类是C#语言对线程对象的封装。在.netframework1.0开始出现。在后面的多线程系列文章中会讲到在不同的.netframework版本中多线程的API使用,在本篇文章中,先来初步认识多线程。
为什么可以使用多线程
1:CPU的多核技术和模拟核技术:
如计算机的参数概念4核8线程,所谓的4核8线程,4核指的是物理核心。通过超线程技术,用一个物理核模拟两个虚拟核,每个核两个线程,总数为8线程。四核八线程采用的超线程技术,是指每个CPU核心没有满负荷运载时,其剩余用量可以模拟成虚拟的核心。单个物理核同一时间点只能处理一个线程,通过超线程技术可以实现单个物理核实现线程级别的并行计算。
2:CPU分片:实际上CPU在同一时刻只能处理一个任务,但是因为CPU的计算能力强大,在1秒内可以响应不同的任务,把1秒的处理能力分成10份,1到100毫秒处理任务A,101到200毫秒处理任务B,201到300毫秒处理任务C…,从宏观角度来看,感觉多个任务在并发执行,这个就是CPU的分片。
初识同步和异步
同步方法:发起调用,完成后才继续下一行;非常符合开发思维,有序由上至下执行;
异步方法:发起调用,不用等待完成,直接进入下一行,启用一个新的线程来完成计算。
同步方法就像真诚的请人吃饭,客人说他有事,需要忙一会儿,请吃饭的人等待客人忙完了再一起吃饭。异步方法就像客气的请人吃饭,客人说他有事,请吃饭的人说那你先去忙吧,然后自己就去吃饭了。
同步和异步的比较
同步方法卡界面,主线程(UI)线程忙于计算,无暇他顾,异步方法不卡界面:主线程闲置,计算任务交给子线程完成,改善用户体验。如在winform中点击按钮采用同步的方式调用一个复杂的任务计算会导致界面短暂卡死,直到任务计算结束才可以操作界面。
在web应用中发个短信通知,记录一个日志,都可以采用异步的方式去执行,客户端不用等到短信发送成功或者日志记录成功才能接受到服务端的响应。
为了能够清楚的说明情况,这里采用测试程序对比的方式,测试程序界面如下:
计算任务:
private void DoSomeThing(string btnName) { Console.WriteLine($"{btnName} 开始,当前线程id:{Thread.CurrentThread.ManagedThreadId}"); long lResult = 0; for (long i = 0; i < 1_000_000_000; i++) { lResult += i; } Console.WriteLine($"{btnName} 结束,当前线程id:{Thread.CurrentThread.ManagedThreadId}"); }
同步方式调用:
private void BtnSync_Click(object sender, EventArgs e) { Console.WriteLine($"btnSync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")}" + $" {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); for (int i = 0; i < 5; i++) { string name = string.Format($"btnSync_Click_{i}"); this.DoSomeThing(name); } stopwatch.Stop(); Console.WriteLine($"同步方法耗时:{stopwatch.ElapsedMilliseconds}"); Console.WriteLine($"DoSomeThing End {Thread.CurrentThread.ManagedThreadId.ToString("00")} " + $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); }
同步方式调用执行结果:
同步方式调用时CPU的使用情况:
异步方式调用:
private void BtnAsync_Click(object sender, EventArgs e) { Console.WriteLine($"btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")}" + $" {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); Stopwatch stopwatch = new Stopwatch(); List<Task> tasks = new List<Task>(); stopwatch.Start(); for (int i = 0; i < 5; i++) { int k = i; tasks.Add(Task.Run(()=> { string name = string.Format($"btnAsync_Click{k}"); this.DoSomeThing(name); })); } Task.Run(()=> { Task.WaitAll(tasks.ToArray()); stopwatch.Stop(); Console.WriteLine($"异步方法耗时:{stopwatch.ElapsedMilliseconds}"); Console.WriteLine($"DoSomeThing End {Thread.CurrentThread.ManagedThreadId.ToString("00")}" + $" {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); }); }
异步方式调用执行结果:
异步方式调用时CPU的使用情况:
总结
同步方法慢,上图耗时(16402毫秒),因为只有一个线程计算。异步方法快,上图耗时(10524毫秒),因为有多个线程参与计算。观察同步和异步调用时的使用情况折线图分析得知:多线程其实就是资源换取性能。在一个应用程序中是不是开启的线程越多越好?不是的,因为开启更多的资源,需要消耗更多的计算机资源,资源是有限的、资源调度也需要消耗资源,就像项目经理管理开发人员保证项目进度,项目经理调度(管理)开发人员也是需要工资的。
一个订单表统计很耗时间,能用多线程优化性能么?不能!这操作只包含一个任务,没办法并行计算,就像一个老师不能同时在两个班级讲课。如果一个操作在查询数据库的同时,需要调用接口、读写硬盘文件、做数据计算,这个可以用多线程优化性能,因为多个任务可以并行计算。
同步方法有序进行,异步多线程无序
启动无序:线程资源是属于非托管资源,是程序向操作系统申请的,由操作系统的调度策略决定,所以启动顺序是随机的,cpu使用同一个线程计算同一个任务,执行时间也是不确定的,so,结束也是无序的。在使用多线程的时候一定要小心,尤其是多线程间有顺序要求的时候通过延迟一点时间(Thread.Sleep())来控制执行顺序,这是不靠谱的。下一篇我们继续学习C#里面的多线程 多线程系列(二)之Thread类