多线程整理
一、简介
1.1、进程
当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。 一个进程是由多个线程组成。
1.2、线程
线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。
1.3、句柄
句柄是Windows系统中对象或实例的标识。这些对象包括模块、应用程序实例、窗口、控制、位图、GDI对象、资源、文件等。
1.4、多线程
1.4.1、概念
程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
多线程一定是运行在多核计算机上。用CPU运行空间换取时间,CPU是分片执行的。单核CPU上谈多线程,都是扯淡。
1.4.2、优点
提高CPU利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。(牺牲空间计算资源,来换取时间)
1.4.3、缺点
- 占用内存多。线程也是程序,所以线程运行需要占用计算机资源,线程越多占用资源也越多。
- 占用CPU多。多线程需要协调和管理,所以需要CPU跟踪线程,消耗CPU资源。
- 多线程存在资源共享问题。线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
- 管理麻烦,容易产生bug。线程太多会导致控制太复杂,最终可能造成很多Bug。
1.4.4、业务场景
- 主线程试图执行冗长的耗时操作,导致系统界面卡顿,客户体验较差。可以新开线程处理冗长的耗时操作。
- 请求别的数据库服务,业务服务等。新开一个线程,让主线程继续干别的事。
- 利用多线程拆分复杂运算,提高计算速度。
1.4.5、不建议使用多线程的业务场景
当单线程能很好处理问题,就不要使用多线程。
1.5、同步/异步
1.5.1、同步方法
线性执行,从上往下依次执行,同步方法执行慢,消耗的计算机资源少。
1.5.2、异步方法
线程和线程之间,不再线型执行,多个线程总的耗时少,执行快,消耗的计算机资源多,各线程执行是无序的。
二、C#中的多线程
2.1、Thread
最早的多线程处理方式,.NET 1.0时代,逐渐被微软抛弃,微软强推Task。
2.1.1、前台线程/后台线程
前台线程:界面关闭,线程随之消失。
后台线程:界面关闭,线程继续执行,完毕后才结束。
2.2、其他关键字
线程优先级、数据槽、内存栅栏
2.2、 ThreadPool
线程池:不需要程序员对线程的数量管控,提高性能,防止滥用,去掉了很多在Thread中没有必要的Api。
2.3、Task
Task出现之前,微软的多线程处理方式有:Thread→ThreadPool→委托的异步调用。.NET 4.0,在ThreadPool的基础上进行的封装,Task的控制和扩展性很强,在线程的延续、阻塞、取消、超时等方面远胜于Thread和ThreadPool。
2.3.1、开启线程方式
1)、new Task().Start()
2)、Task.Run()
3)、Task.Factory.StartNew()
4)、new Task().RunSynchronously()(同步方式,上面三种异步方式)
2.3.2、线程等待
1、task.Wait()
等待task内部执行完毕,才会往后直行,卡主线程,Task实例方法。
task.Wait(1000);//等待1000毫秒后就往后执行,不管有没有执行结束。
task.Wait(TimeSpan.FromMilliseconds(1000));//等待1000毫秒后就往后执行,不管有没有执行结束。
2、WaitAny
某一个任务执行结束后,去触发一个动作,卡主线程,Task静态方法。数据有可能是来自于第三方接口,缓存,数据库,查询的时候,我们不确定,开启几个线程同时查询,只要一个返回了就返回界面。
3、WaitAll
所有任务执行完成后,去触发一个动作,卡主线程,Task静态方法
数据是来自于第三方接口,缓存,数据库,查询的时候,开启几个线程同时查询,等所有数据全部查询出来,一起返回界面
4、WhenAny
与下面ContinueWith配合执行,当传入的线程中任何一个线程执行完毕,继续执行ContinueWith中的任务(属于开启新线程,不卡主线程),Task静态方法。
5、WhenAll
当其中所有线程执行完成后,新开启了一个线程执行,继续执行新业务,所以执行过程中,不卡主线程,Task静态方法。
List<Task> taskList = new List<Task>(); TaskFactory factory = new TaskFactory(); taskList.Add(factory.StartNew(() => { Debug.WriteLine("查询数据库"); })); taskList.Add(factory.StartNew(() => { Debug.WriteLine("查询缓存"); })); taskList.Add(factory.StartNew(() => { Debug.WriteLine("查询接口"); })); Task.WhenAll(taskList.ToArray()).ContinueWith((n) => { Debug.WriteLine($"查到数据,返回界面!"); });
6、ContinueWhenAny
某一个任务执行结束后,去触发一个动作,不卡主线程,TaskFactory实例方法,等价于WhenAny+ContinueWith
List<Task> taskList = new List<Task>(); TaskFactory factory = new TaskFactory(); taskList.Add(factory.StartNew(obj => Coding("张三", "数据库设计"), "张三")); taskList.Add(factory.StartNew(obj => Coding("李四", "接口对接"), "李四")); taskList.Add(factory.StartNew(obj => Coding("王五", "Webapi"), "王五")); taskList.Add(factory.StartNew(obj => Coding("赵六", "前端页面"), "赵六")); factory.ContinueWhenAny(taskList.ToArray(), ts => { Debug.WriteLine($"{ts.AsyncState}同学开发完毕,田七开始测试!"); });
7、ContinueWhenAll
所有任务执行完成后,去触发一个动作,不卡主线程,TaskFactory实例方法,等价于WhenAll+ContinueWith
List<Task> taskList = new List<Task>(); TaskFactory factory = new TaskFactory(); taskList.Add(factory.StartNew(obj => Coding("张三", "数据库设计"), "张三")); taskList.Add(factory.StartNew(obj => Coding("李四", "接口对接"), "李四")); taskList.Add(factory.StartNew(obj => Coding("王五", "Webapi"), "王五")); taskList.Add(factory.StartNew(obj => Coding("赵六", "前端页面"), "赵六")); factory.ContinueWhenAll(taskList.ToArray(), ts => { Debug.WriteLine($"所有人开发完毕,我们一起庆祝一下吃个饭!"); });
2.3.3、TaskCreationOptions枚举类详解
一个Task内部,可以开启线程,Task内部的线程可以理解为子线程,Task为父线程,创建Task实例的时候可以传入TaskCreationOptions枚举参数来影响线程的运行方式
1、None,默认情况
父线程不会等待子线程执行结束才结束。
Task task = new Task(() => { Debug.WriteLine($"task--start--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); Task task1 = new Task(() => { this.DoSomething("task1"); }); Task task2 = new Task(() => { this.DoSomething("task2"); }); task1.Start(); task2.Start(); Debug.WriteLine($"task--end--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); }); task.Start(); task.Wait();
2、AttachedToParent
子线程附加到父线程,父线程必须等待所有子线程执行结束才能结束,相当于Task.WaitAll(task1, task2)。
Task task = new Task(() => { Debug.WriteLine($"task--start--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); Task task1 = new Task(() => { this.DoSomething("task1"); }, TaskCreationOptions.AttachedToParent); Task task2 = new Task(() => { this.DoSomething("task2"); }, TaskCreationOptions.AttachedToParent); task1.Start(); task2.Start(); Debug.WriteLine($"task--end--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); }); task.Start(); task.Wait();
3、DenyChildAttach
不允许子任务附加到父任务上,反AttachedToParent,和默认效果一样
Task task = new Task(() => { Debug.WriteLine($"task--start--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); Task task1 = new Task(() => { this.DoSomething("task1"); }, TaskCreationOptions.AttachedToParent); Task task2 = new Task(() => { this.DoSomething("task2"); }, TaskCreationOptions.AttachedToParent); task1.Start(); task2.Start(); Debug.WriteLine($"task--end--{Thread.CurrentThread.ManagedThreadId.ToString("00")}--{DateTime.Now.ToString("HH:mm:ss.fff")}"); }, TaskCreationOptions.DenyChildAttach); task.Start(); task.Wait();
4、PreferFairness
相对来说比较公平执行的先申请的线程优先执行
Task task1 = new Task(() => { this.DoSomething("task1"); }, TaskCreationOptions.PreferFairness); Task task2 = new Task(() => { this.DoSomething("task2"); }, TaskCreationOptions.PreferFairness); task1.Start(); task2.Start();
5、LongRunning
事先知道是长时间执行的线程就加这个参数,线程调度会优化
6、RunContinuationsAsynchronously
强制以异步方式执行添加到当前任务的延续。
7、HideScheduler
防止环境计划程序被视为已创建任务的当前计划程序。 这意味着像 StartNew 或 ContinueWith 创建任务的执行操作将被视为 System.Threading.Tasks.TaskScheduler.Default当前计划程序。
8、TaskContinuationOptions枚举类详解
ContinueWith可以传入TaskContinuationOptions枚举类参数来影响线程的运行方式。
(1)、None,默认情况
任务顺序执行
Task task1 = new Task(() => { this.DoSomething("task1"); }); Task task2 = task1.ContinueWith(t => { this.DoSomething("task2"); }); Task task3=task2.ContinueWith(t => { this.DoSomething("task3"); }); task1.Start();
(2)、LazyCancellation
取消该线程,该线程的前一个线程和后一个线程顺序执行。
CancellationTokenSource source = new CancellationTokenSource(); source.Cancel(); Task task1 = new Task(() => { this.DoSomething("task1"); }); Task task2 = task1.ContinueWith(t => { this.DoSomething("task2"); }, source.Token,TaskContinuationOptions.LazyCancellation, TaskScheduler.Current); Task task3 = task2.ContinueWith(t => { this.DoSomething("task3"); }); task1.Start();
(3)、ExecuteSynchronously
前后任务由同一个线程执行
Task task1 = new Task(() => { this.DoSomething("task1"); }); Task task2 = task1.ContinueWith(t => { this.DoSomething("task2"); },TaskContinuationOptions.ExecuteSynchronously); task1.Start();
(4)、NotOnRanToCompletion
Task task1 = new Task(() => { this.DoSomething("task1"); //异常了,表示未执行完成,task2能执行 //不异常,表示执行完成,task2不能执行 throw new Exception("手动制造异常,表示不能执行完毕"); }); Task task2 = task1.ContinueWith(t => { this.DoSomething("task2"); }, TaskContinuationOptions.NotOnRanToCompletion); task1.Start();
(5)、OnlyOnRanToCompletion
延续任务必须在前面task完成状态才能执行,和NotOnRanToCompletion正好相反
Task task1 = new Task(() => { this.DoSomething("task1"); //异常了,表示未执行完成,task2不能执行 //不异常,表示执行完成,task2能执行 throw new Exception("手动制造异常,表示不能执行完毕"); }); Task task2 = task1.ContinueWith(t => { this.DoSomething("task2"); }, TaskContinuationOptions.OnlyOnRanToCompletion); task1.Start();
(6)、NotOnFaulted
延续任务必须在前面task完成状态才能执行,效果和OnlyOnRanToCompletion差不多。
Task task1 = new Task(() => { this.DoSomething("task1"); //throw new Exception("手动制造异常"); }); Task task2 = task1.ContinueWith(t => { this.DoSomething("task2"); }, TaskContinuationOptions.NotOnFaulted); task1.Start();
(7)、OnlyOnFaulted
延续任务必须在前面task未完成状态才能执行,效果和NotOnRanToCompletion差不多。
Task task1 = new Task(() => { this.DoSomething("task1"); //throw new Exception("手动制造异常"); }); Task task2 = task1.ContinueWith(t => { this.DoSomething("task2"); }, TaskContinuationOptions.OnlyOnFaulted); task1.Start();
(8)、OnlyOnCanceled
前面的任务未被取消才执行后面的任务。
CancellationTokenSource cts = new CancellationTokenSource(); Task task1 = new Task(() => { this.DoSomething("task1"); cts.Cancel(); }); Task task2 = task1.ContinueWith(t => { this.DoSomething("task2"); }, TaskContinuationOptions.OnlyOnCanceled); task1.Start();
(9)、NotOnCanceled
前面的任务被取消才执行后面的任务
CancellationTokenSource cts = new CancellationTokenSource(); Task task1 = new Task(() => { this.DoSomething("task1"); cts.Cancel(); }); Task task2 = task1.ContinueWith(t => { this.DoSomething("task2"); }, TaskContinuationOptions.NotOnCanceled); task1.Start();
(10)、PreferFairness
System.Threading.Tasks.TaskScheduler 以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。
(11)、LongRunning
指定某个任务将是运行时间长、粗粒度的操作。 它会向 System.Threading.Tasks.TaskScheduler 提示,过度订阅可能是合理的。
(12)、AttachedToParent
指定将任务附加到任务层次结构中的某个父级。
(13)、DenyChildAttach
如果尝试附有子任务到创建的任务,指定 System.InvalidOperationException 将被引发。
(14)、HideScheduler
防止环境计划程序被视为已创建任务的当前计划程序。 这意味着像 StartNew 或 ContinueWith 创建任务的执行操作将被视为 System.Threading.Tasks.TaskScheduler.Default当前计划程序。
9、延迟执行
Task.Delay(),一般和ContinueWith配合使用,执行的动作就是ContinueWith内部的委托,委托的执行有可能是一个全新的线程,也有可能是主线程。
//开启线程后,线程等待3000毫秒后执行动作,不卡主线程 Task.Delay(3000).ContinueWith(t => { this.DoSomething("张三"); });
2.3.4、Task进阶
1、多线程捕获异常
1)、线程不等待,捕捉不到异常
多线程中,如果发生异常,使用try-catch包裹,捕捉不到异常,异常还没发生,主线程已经执行结束。
//捕捉不到异常 try { Task task = Task.Run(() => { int i = 0; int j = 10; int k = j / i; //尝试除以0,会异常 }); } catch (AggregateException aex) { foreach (var exception in aex.InnerExceptions) { Debug.WriteLine($"线程不等待:异常{exception.Message}"); } }
2)、线程不等待,线程内部捕获异常
多线程中,如果要捕捉异常,可以在线程内部try-catch,可以捕捉到异常。
//捕捉到异常 try { Task task = Task.Run(() => { try { int i = 0; int j = 10; int k = j / i; //尝试除以0,会异常 } catch (Exception ex) { Debug.WriteLine($"线程内异常{ex.Message}"); } }); } catch (AggregateException aex) { foreach (var exception in aex.InnerExceptions) { Debug.WriteLine($"线程不等待:异常{exception.Message}"); } }
运行结果
线程内异常Attempted to divide by zero.
3)、线程等待,能够捕获异常
-
-
-
-
多线程中,如果要捕捉异常,需要设置主线程等待子线程执行结束,可以捕捉到异常
-
多线程内部发生异常后,抛出的异常类型是system.AggregateException
-
-
-
//捕捉到异常 try { Task task = Task.Run(() => { int i = 0; int j = 10; int k = j / i; //尝试除以0,会异常 }); //线程等待 task.Wait(); } catch (AggregateException aex) { foreach (var exception in aex.InnerExceptions) { Debug.WriteLine($"线程等待:异常{exception.Message}"); } }
运行结果:
线程等待:异常Attempted to divide by zero.
2、线程取消
线程取消是不能从外部取消的,线程取消的实质还是通过变量去控制程序的运行和结束,正常结束,或者发生异常结束
其他参考:链接