一、直接使用线程的问题
- 每次都要创建Thread对象,并向操作系统申请创建一个线程,这是需要耗费CPU时间和内存资源的。
- 无法直接获取线程函数返回值
- 无法直接捕捉线程函数内发生的异常
使用线程池可以解决第一个问题
二、.NET中的线程池
在这里只简单的介绍一下ThreadPool,由于TPL的存在,我工作中大部分使用的是TPL中的类,这是后面介绍的重点。
1. ThreadPool.QueueUserWorkItem
这个方法有三个重载
- public static bool QueueUserWorkItem(WaitCallback callBack)
- public static bool QueueUserWorkItem(WaitCallback callBack, object? state)
- public static bool QueueUserWorkItem<TState>(Action<TState> callBack, TState state, bool preferLocal)
第一个函数需要传入一个WaitCallback 委托,该委托的定义如下
- public delegate void WaitCallback(object? state);
第二个函数多了一个state参数,表示需要传给委托的参数,若无需传参调用第一个函数即可。
第三个函数是一个泛型版本,还多了一个布尔类型的参数preferLocal,这个参数表示传入的委托将会在放入线程池工作线程的本地队列还是线程池的全局队列。
线程池内部有本地队列和全局队列的概念,线程池遵循生产者-消费者模式,线程池还可以为线程数量提供良好的伸缩性,有关.NET线程池的详细信息,请参见https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.threadpool?view=netcore-3.1
需要注意的是,线程池中的线程默认为后台线程,这意味着如下代码一般不能按预期工作。
1 static void Main(string[] args) 2 { 3 ThreadPool.QueueUserWorkItem(state => 4 { 5 Thread.Sleep(100); 6 Console.WriteLine("执行完毕"); 7 }); 8 }
并且由于线程会被复用,所以不能作依赖于某个特定线程的操作。
2. 何时不使用线程池(摘录微软文档)
- 需要一个前台线程。
- 需要具有特定优先级的线程。
- 拥有会导致线程长时间阻塞的任务。 线程池具有最大线程数,因此大量被阻塞的线程池线程可能会阻止任务启动。
- 需将线程放入单线程单元。 所有 ThreadPool 线程均位于多线程单元中。
- 需具有与线程关联的稳定标识,或需将一个线程专用于一项任务。
三、更方便的解决方案(使用TPL)
1.处理线程池未解决的问题
线程池虽然解决了线程资源浪费的问题,但是以下两点还未解决
- 无法直接获取线程函数返回值
- 无法直接捕捉线程函数内发生的异常
在上一篇中MyTask类可以解决这两个问题,但由于内部是每个Task直接开一个线程,资源浪费的问题还是没有解决,所以我们是不是能够把两者结合呢?
2.Task初体验
无返回值无异常
1 static void Main(string[] args) 2 { 3 Task task = new Task(() => 4 { 5 Thread.Sleep(100); 6 Console.WriteLine($"是否是线程池线程:{Thread.CurrentThread.IsThreadPoolThread}"); 7 }); 8 task.Start(); 9 try 10 { 11 task.Wait(); 12 } 13 catch(Exception e) 14 { 15 Console.WriteLine(e.Message); 16 } 17 }
输出如下:
无返回值有异常
1 static void Main(string[] args) 2 { 3 var task = new Task(() => 4 { 5 Console.WriteLine($"是否是线程池线程:{Thread.CurrentThread.IsThreadPoolThread}"); 6 var task1 = new Task(() => 7 { 8 Console.WriteLine($"是否是线程池线程:{Thread.CurrentThread.IsThreadPoolThread}"); 9 throw new Exception("延续任务发生异常"); 10 },TaskCreationOptions.AttachedToParent); 11 task1.Start(); 12 throw new Exception("主任务发生异常"); 13 }); 14 15 task.Start(); 16 try 17 { 18 task.Wait(); 19 } 20 catch(AggregateException ae)//Task内部包装了异常,有异常发生Wait()内部会抛出一个聚合异常 21 { 22 foreach(var e in ae.Flatten().InnerExceptions)//把阶梯式的聚合异常变为扁平的异常 23 { 24 Console.WriteLine(e.Message); 25 } 26 } 27 }
有返回值的就不演示了。可以看到,使用Task解决了开始的三种问题,但事物总是具有两面性,有优点也有缺点,Task会带来额外的内存分配,Task抽象层次过高,深入理解并使用好并非易事,在与async/await关键字配合编写异步代码时更加突出。
网上关于Task的使用例子很多,微软文档也很全,我在此这里补充一下需要注意的地方。
- 任务是托管线程上更高层次的抽象
- 任务的执行由任务调度器(TaskScheduler)决定
- 默认的任务调度器是线程池调度器,它使用线程池执行任务
- Task.Run()静态方法和Task.Start()实例方法以及默认的任务工厂Task.Factory都是使用默认的线程池任务调度器
- 因此说Task是对线程池的封装是不准确的
- 通过指定TaskCreationOptions.LongRunning枚举便可让任务在非线程池中的线程上执行,这样可以避免长期占用线程池中的线程,因为线程池是有大小的,一般线程池用来处理简单但量多的工作。