探究 .NET 的多线程 Thread、ThreadPool、Task、Parallel、await/async

.NET 的多线程 Thread、ThreadPool、Task、Parallel、await/async探究

  • 进程
  • 线程
    • 操作系统为什么可以多线程
    • 并发并行
    • 同步异步
    • 异步方法
    • 线程1.0 Thread
    • 线程2.0 线程池 ThreadPool
    • 线程3.0 Task
    • 线程4.0 Parallel
    • 多线程的异常捕获
    • 线程取消
    • 临时变量问题
    • 线程安全&Lock
    • await/async

进程

进程是一个应用程序运行的实例,程序在服务器运行时占据全部计算资源总和,是一个计算机概念

线程

进程在响应操作时最小单位,也包含CPU,内存,网络,硬盘,IO,也是一个计算机概念。一个进程包含多个线程,线程属于进程,进程销毁线程也就没了

  • 句柄:
    其实是一个long数字,是操作系统标识应用程序的
  • 多线程:
    一个进程多个线程同时运行。

Thread类是C#语言对线程对象的一个封装

操作系统为什么可以多线程

  1. 多个CPU的核可以并行工作
    4核8线程,这里的线程指的是模拟核
  2. CPU分片
    1秒的处理能力分成1000份,系统调度去响应不同的任务,从宏观角度来说,感觉就是多个任务在并发执行。从微观角度来说,一个物理CPU同一时刻只能为一个任务服务。

并发并行

  • 并行:多核之间叫并行,每个CPU的核执行的操作
  • 并发: CPC分片的并发, 一个CPU让多个程序运行

同步异步

  • 同步
    发起调用,完成后才继续,非常符合开发思维,有序执行

  • 异步
    发起调用,不等待完成,直接进入下一行执行,启动一个线程来完成方法的计算

异步方法

  1. 同步方法卡界面:主线程(UI线程)忙于计算,无暇他顾
    异步多线程方法不卡界面:主线程闲置,计算任务交给子线程完成,改善用户体验,winform点击按钮不至于卡死
    web应用发个短信通知,使用异步多线程完成发短信的操作

  2. 同步方法慢,只有一个线程计算
    异步多线程方法块,因为多个线程并发计算
    多线程其实是资源换性能。①资源不是无限的;②资源调度损耗
    一个订单表统计很耗时间,能不能多线程优化下性能? 答案是不能的,这就是一个操作,没法并行
    需要查询数据库/调度接口/读硬盘文件/做数据计算,能不能多线程优化下性能。这个答案是可以的。多个任务是可以并行的
    线程不是越多越好,因为资源有限,而且调度有损耗

  3. 同步方法由于进行,异步多线程无序
    启动无序:线程资源是向操作系统申请的,有操作系统的调度策略决定,所以启动顺序随机
    同一任务同一个线程,执行时间也不确定,因为CPU的分片
    以上结论所以结果也是无序的
    使用多线程请一定小心,很多事不是想当然的,尤其是多线程操作间有顺序要求的时候,通过延迟一点启动来控制顺序?或者预计下结束顺序?这些都不靠谱

线程1.0 Thread

.NET Framework 1.0出现

{
	ThreadStart method = ()=>{this.DoSomething()};
	Thread thread = new Thread(method);
	thread.Start();//开启线程
	thread.Suspend();//暂停(弃用)
	thread.Resume();//恢复(弃用)  真的不该要的,暂停一下不一定马上暂停;让线程操作太复杂了
	thread.Abort();//线程销毁
	Thread.ResetAbort();//恢复销毁的线程
	thread.Join();//等待线程
	thread.Join(1000);//最多等待1000ms
	thread.Priority=ThreadPriority.Higheset;//最高优先级;优先执行,但不代表优先完成,甚至极端情况下,还有意外发生,不通过这个来控制线程的执行顺序
	thread.IsBackgroud=false;//是否后台线程;默认是false 前台线程。进程关闭,线程需要计算完成后才退出。
}

{
	//基于thread封装一个回调
	//回调:启动子线程执行动作A--不阻塞--A执行完后子线程执行动作B
	private viod ThreadWithCallBack(TreadStart threadStart,Action actionCallback)
	{
		ThreadStart method = new ThreadStart(()=>{
			threadStart();
			actionCallback();
		})
		new Thread(method).Start();
	}
}
{
	//基于thread封装一个异步、非阻塞、还能获取返回值的的方法
	private Func<T> ThreadWithReturn<T>(Func<T> func)
	{
		T t= default(T);
		 ThradStart threadStart = new ThreadStart(()=>{
			t = func();
		})
		Thread thread = new Thread(threadStart);
		thread.Start();
		return new Func<T>(()=>{
			thread,Join();
			return t;
		});
	}
}

线程2.0 线程池 ThreadPool

Thread功能繁多,反而用不好–就像给4岁小孩一把热武器,反而会造成更大的伤害
对线程数量没有管控

线程池 .NET Freamwork 2.0出现
如果某个对象创建和销毁代价比较高,同时这个对象还可以反复使用的,就需要一个池子保存多个这样的对象,需要用的时候从长池子里面获取;用完之后不用销毁,放回池子
节约资源提升性能,此外还能管控总数量,防止滥用

线程池都是后台线程

{
	ThreadPool.QueueUserWorkItem(o=>{this.DoSomething()});//不带参数
	ThreadPool.QueueUserWorkItem(o=>{this.DoSomething(o)},"xxx");//不带参数
	ThreadPool.GetMaxThreads(out int workerThreads,out int completionPortThreads);//当前电脑最大线程数/当前电脑最大异步/IO线程数
	ThreadPool.GetMinThreads(out int workerThreads,out int completionPortThreads);//当前电脑最小线程数/当前电脑最小异步/IO线程数
	
	//设置的线程池数据量是全局的
	//委托异步调用-Task-Parrallel-async/await 全部都是线程池的线程
	//直接new Thread不受这个数量的限制的(但是会占用线程池的线程数量)
	ThreadPool.SetMaxThreads(12,12);//设置最大线程数
	ThreadPool.SetMinThreads(2,2);//设置最小线程数
	
	//等待
	ManualResetEvent mre = new ManualResetEvent(false);
	//false -- 关闭--Set打开--true--WaitOne就能通过
	//true  -- 打开--ReSet关闭--false--WaitOne就只能等待
	ThreadPool.QueueUserWorkItem(o=>{this.DoSomething();mre.Set();});
	mre.WaitOne();
}

线程3.0 Task

Task是 .NET Freamwork 3.0出现的,线程是基于线程池,提供了丰富的API

Task线程启动方式:

{
	//方法一
	Task task = new Task(()=>this.DoSomething());
	task.Start();
	//方法二
	Task task =Task.Run(()=>this.DoSomething());
	//方法三
	TaskFactory taskFactory = Task.Factory;
	Task task=taskFactory.StartNew(()=>this.DoSomething());
	//方法四,带返回值
	Task<int> res = Task.Run<int>(()=>{return 2});
	int i = res.Result;//会阻塞
}

Task常用API:

Thread.Sleep(2000);//同步等待,阻塞当前线程
Task.Delay(2000);//异步等待,不阻塞当前线程
//不阻塞当前线程等待2000ms,执行ContinueWith里面的委托
Task.Delay(2000).ContinueWith(t=>{
	this.DoSomething();
});

//等待
List<Task> taskList= new List<Task>();
TaskFactory taskFactory = Task.Factory;
Task.WaitAll(taskList.toArray());//方法一:等待全部完成
Task.WaitAny(taskList.toArray());//方法二:等待任意一个任务完成

//回调
taskFactory.ContinueWhenAll(taskList.toArray(),t=>{this.DoSomething();});//一组任务完成时调用回调方法
taskFactory.ContinueWhenAny(taskList.toArray(),t=>{this.DoSomething();});//一组任务完成y一个时调用回调方法
taskFactory.ContinueWith(t=>{this.DoSomething()});//单个任务回调

//不使用线程池控制线程任务数量,10000个任务,20个线程数量
List<Task> taskList= new List<Task>();
for(int i=0;i<10000,i++){
	if(taskList.Count(t=>t.Status!=TaskStatus.RanToCompletion)>20){
		//等待任务完成
		Task.WaitAny(taskList.toArray());
		//清理已完成的任务
		taskList = taskList.where(t=>t.Status!=TaskStatus,RanToCompletion).toList();
	}
	taskList.Add(Task.Run(t=>{
		this.DoSomething()
	}));
}
  • 什么时候能要多线程?
    在任务能并发的时候

  • 多线程能干嘛?
    提升速度/优化用户体验

  1. 当任务有严格的执行顺序时,不能并发所以不能使用多线程;
  2. 当多个任务可以合作时,就可以选择使用多线程来提升性能;

线程4.0 Parallel

Parallel是 .NET Freamwork 4.0出现的
Parallel并发执行多个Action 多线程的
主线程会参与计算–所以会阻塞界面
等于TaskWaitAll + 主线程计算

//方法一
Parallel.Invoke(()=>this.DoSomething(),
()=>this.DoSomething(),
()=>this.DoSomething(),
()=>this.DoSomething());

//方法二
Parallel.For(0,5,i=>this.DoSomething());

//方法三
Parallel.ForEach(new int[]{0,1,2,3,4},i=>this.DoSomething());

//方法四
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism=3;//指定最大任务数
Parallel.For(0,15,i=>this.DoSomething());

//Parallel是阻塞的,有没有办法不阻塞?包一层!
Task.Run(()=>{
	Parallel.For(0,5,i=>this.DoSomething())
});

多线程的异常捕获

多线程里面抛出的异常,会终结当前线程,但是不会影响别的线程
那线程去哪里了呢?当然是被吞了。
假如想捕获异常信息,或者还要通知别的线程该怎么办呢

List<Task> taskList= new List<Task>();
try
{
	for(int i=0;i<100,i++){
		string name = i.toString();
		taskList.Add(Task.Run(()=>{
			if(name.Equals("3")){
				throw new Execption("3异常");
			}
			else if(name.Equals("11")){
				throw new Execption("11异常");
			}
			else if(name.Equals("55")){
				throw new Execption("55异常");
			}
			Console.WriteLine(name);
		}));
	}
	Task.WaitAll(taskList.toArray());

}
//捕获多线程异常
catch(AggregateExecption aex ){
	foreach(var execption in aex.InnerExecptions){
		Console.WriteLine(execption.Message);
	}
}
catch(Execption ex){
	Console.WriteLine(ex.Message);
}

线程异常后经常是需要通知别的线程,而不是等到WaitAll,问题就是要取消多线程
工作中常规建议:多线程的委托里面不允许出现异常,通常在多线程的委托里面包一层try-catch,然后记录下来异常信息,完成需要的操作

List<Task> taskList= new List<Task>();

for(int i=0;i<100,i++){
	string name = i.toString();
	taskList.Add(Task.Run(()=>{
		try{
			if(name.Equals("3")){
				throw new Execption("3异常");
			}
			else if(name.Equals("11")){
				throw new Execption("11异常");
			}
			else if(name.Equals("55")){
				throw new Execption("55异常");
			}
			Console.WriteLine(name);
		}
		cathc(Execption ex){
			Console.WriteLine(ex.Message);
		}
	}));
}

线程取消

多线程并发任务,某个失败后,希望通知别的线程,都停下来
在Thread时代的时候,使用Thread.Abort()–终止线程,向当前线程抛出一个异常然后终结任务;线程属于OS资源,可能不会立即停下来
Task不能外部终止任务,只能自己终结自己
CancellationTokenSource.Token能让没有启动的任务取消

List<Task> taskList= new List<Task>();
//1.准备cts
CancellationTokenSource cts = new CancellationTokenSource();
for(int i=0;i<10,i++){
	string name = i.toString();
	taskList.Add(Task.Run(()=>{
		//2.准备try-catch
		try{
			if(!cts.IsCancellationRequested)
				Console.WriteLine($"{name} 开始");
			Thread.Sleep(5000);
			if(name.Equals("3")){
				throw new Execption("3异常");
			}
			else if(name.Equals("11")){
				throw new Execption("11异常");
			}
			else if(name.Equals("55")){
				throw new Execption("55异常");
			}
			//3. Action判断IsCancellationRequested尽快停止,肯定有延迟,在判断环境才会借宿
			if(cts.IsCancellationRequested){
				Console.WriteLine($"{name} 结束");
			}
			else{
				Console.WriteLine($"{name} 被取消");
			}
		}
		cathc(Execption ex){
			Console.WriteLine(ex.Message);
			cts.Cancel();
		}
	},cts.Token));//cts.Token在线程出现异常的时候可以让没有启动任务取消
}
Task.WaitAll(taskList.toArray());

临时变量问题

临时变量问题,线程是非阻塞的,延迟启动的;线程执行的时候 i 已经是5了
k是闭包里面的变量,每次循环都有一个独立的k
5个k变量,一个 i 变量

for(int i=0;i<5,i++){
	int k=i;
	Task.Run(()=>{
		Console.WriteLine($"i:{i},k:{k}");
	})
}

线程安全&Lock

线程安全:如果你的代码在进程中有多个线程同时运行这一段,如果每个运行的结果都跟单线程运行时的结果一直,那么就线程安全的
线程安全问题一般都是全局变量/共享变量/静态变量/硬盘文件/数据库数据,只要多线程同时访问和修改造成的

  1. lock
    lock是语法糖,Monitor.Enter,占据一个引用,别的线程就只能等着
    推荐锁是private static readonly object
    A 不能是 null,可以编译但是不能运行
    B lock(this),在外面如果也要用实例,就冲突了,不推荐
    C 不应该是string; string在内存分配上是重用的,会冲突
    D lock里面的代码不要太多,lock里面是单线程的

  2. 线程安全集合
    System.Collections.Concurrent.ConcurrentQueue

  3. 数据分拆,避免多线程操作同一个数据,又安全有高效

await/async

await/async 是 .NET Framework 4.5出现的,是一个语法糖

  1. await/async语法和使用;
    await/async 是c#的保留关键字,通常成对出现;
    async修饰方法,可以单独出现,但是有警告;
    await在方法体,只能出现在task/async方法前面,只有await会报错;
    主线程调用await/async方法,主线程遇到await返回执行后续动作,await后面的代码会等着task任务的完成后再继续执行;
    其实就是把await后面的代码包装成一个continueWith的回调动作;
    然后这个回调动作可能是Task线程,也可能是主线线程,还可能是新的子线程;
    一个async方法,如果没有返回值,可以方法声明返回Task;
    await/async能够用同步的方式编写代码,但又是非阻塞的;

  2. 原理探究和使用建议
    async方法在编译后会生成一个状态机(实现了IAsyncStateMachine接口)
    ① async方法里面的逻辑其实都放在MoveNext里面
    ② 主线程new一个状态机,初始化状态为-1
    ③ 主线程调用MoveNext,来执行await之前的逻辑
    ④ 主线程将状态改为0,再启动一个Task任务,继续回调用方法的地方执行后续代码
    ⑤ 子线程调用MoveNext,将状态改为-1,再去执行await之后的代码

你可能感兴趣的:(.NET,.NET,Core,.net,c#,多线程,并发编程)