赞
踩
这三者都是为了处理耗时任务,且都是异步的。
Thread就是Thread,需要自己调度,适合长跑型的操作。
ThreadPool是Thread基础上的一个线程池,目的是减少频繁创建线程的开销。线程很贵,要开新的stack,要增加CPU上下文切换,所以ThreadPool适合频繁、短期执行的小操作。调度算法是自适应的,会根据程序执行的模式调整配置,通常不需要自己调度线程。另外分为Worker和IO两个池。IO线程对应Native的overlapped io,Win下利用IO完成端口实现非阻塞IO。
【Thread vs. ThreadPoll】
前台线程:主程序必须等待线程执行完毕后才可退出程序。Thread默认为前台线程,也可以设置为后台线程。
后台线程:主程序执行完毕后就退出,不管线程是否执行完毕。ThreadPool默认为后台线程。
线程消耗:开启一个新线程,线程不做任何操作,都要消耗1M左右的内存。
总结:ThreadPoll 性能优于 Thread,但是 Thread 和 ThreadPoll 对线程的控制都不是很好,例如线程等待(线程执行一段时间无响应后,直接停止线程),释放资源等,都没有直接的API来控制,只能通过硬编码来实现。同时,ThreadPool 使用的是线程池全局队列,全局队列中的线程依旧会存在竞争共享资源的情况,从而影响性能。
Task 是在 .NET Framework 4 中添加进来的。这是新的 namespace:System.Threading.Tasks;它强调的是 adding parallelism and concurrency to applications。在语法上,和 lamda 表达式更好地结合。
Task 背后的实现也是使用了线程池线程,但它的性能优于ThreadPoll,因为它使用的不是线程池的全局队列,而是本地队列,使线程之间的资源竞争减少。同时,Task 提供了丰富的 API 来管理、控制线程。但是相对前面两种耗内存,Task 依赖于 CPU,对于多核的CPU性能远超前两者,而对于单核的CPU,三者性能没什么差别。
创建Task有两种方法:
- //工厂创建,直接执行
- Task t = Task.Factory.StartNew(() => {
-
- Console.WriteLine("任务已启动....");
-
- });
或者
- //直接实例化,必须手动去Start
- Task t2 = new Task(() => {
-
- Console.WriteLine("开启一个新任务");
-
- });
-
- t2.Start();//任务已启动...
第一种方法不需要调用start,初始化后任务就开始了。
Task还可以随时取消正在执行的任务,请看下面的代码
- static void Main(string[] args) {
- CancellationTokenSource cts = new CancellationTokenSource();
- Task t3 = new Task(() => LongRunTask(cts.Token));
- t2.Start();
- Thread.Sleep(3000);
- cts.Cancel();
- Console.Read();
- }
-
- private static void LongRunTask(CancellationToken token) {
- while (true) {
- if (!token.IsCancellationRequested) {
- Thread.Sleep(500);
- Console.WriteLine(".");
- } else {
- Console.WriteLine("任务取消了");
- break;
- }
- }
- }

方法名 | 说明 |
---|---|
Task.Wait | task1.Wait(); 就是等待任务执行(task1)完成,task1的状态变为Completed |
Task.WaitAll | 待所有的任务都执行完成 |
Task.WaitAny | 同Task.WaitAll,就是等待任何一个任务完成就继续向下执行 |
Task.ContinueWith | 第一个Task完成后自动启动下一个Task,实现Task的延续 |
CancellationTokenSource | 通过cancellation的tokens来取消一个Task |
【Task 的优势】
ThreadPool 相比Thread来说具备了很多优势,但是ThreadPool却又存在一些使用上的不方便。比如:
1. ThreadPool 不支持线程的取消、完成、失败通知等交互性操作;
2. ThreadPool 不支持线程执行的先后次序。
以往,如果开发者要实现上述功能,需要完成很多额外的工作,现在,FCL中提供了一个功能更强大的概念:Task。Task在线程池的基础上进行了优化,并提供了更多的API。在 FCL4.0 中,如果我们要编写多线程程序,Task显然已经优于传统的方式。
以下是一个简单的任务示例:
- static void Main(string[] args)
- {
- Task t = new Task(() =>
- {
- Console.WriteLine("任务开始工作……");
- //模拟工作过程
- Thread.Sleep(5000);
- });
- t.Start();
- t.ContinueWith((task) =>
- {
- Console.WriteLine("任务完成,完成时候的状态为:");
- Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}",
- task.IsCanceled, task.IsCompleted, task.IsFaulted);
- });
- Console.ReadKey();
- }

【Task的完成状态】
任务 Task 有这样一些属性,让我们查询任务完成时的状态:
1: IsCanceled,因为被取消而完成;
2: IsCompleted,成功完成;
3: IsFaulted,因为发生异常而完成。
需要注意的是,任务并没有提供回调事件来通知完成(像 BackgroundWorker 一样),它通过启用一个新任务的方式来完成类似的功能。ContinueWith 方法可以在一个任务完成的时候发起一个新任务,这种方式天然就支持了任务的完成通知:可以在新任务中获取原任务的结果值。
下面是一个稍微复杂一点的例子,同时支持完成通知、取消、获取任务返回值等功能:
- static void Main(string[] args)
- {
- CancellationTokenSource cts = new CancellationTokenSource();
- Task<int> t = new Task<int>(() => Add(cts.Token), cts.Token);
- t.Start();
- t.ContinueWith(TaskEnded);
- //等待按下任意一个键取消任务
- Console.ReadKey();
- cts.Cancel();
- Console.ReadKey();
- }
-
- static void TaskEnded(Task<int> task)
- {
- Console.WriteLine("任务完成,完成时候的状态为:");
- Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}",
- task.IsCanceled, task.IsCompleted, task.IsFaulted);
- Console.WriteLine("任务的返回值为:{0}", task.Result);
- }
-
- static int Add(CancellationToken ct)
- {
- Console.WriteLine("任务开始……");
- int result = 0;
- while (!ct.IsCancellationRequested)
- {
- result++;
- Thread.Sleep(1000);
- }
- return result;
- }

在任务开始后大概3秒钟的时候按下键盘,会得到如下的输出:
任务开始……
任务完成,完成时候的状态为:
IsCanceled=False IsCompleted=True IsFaulted=False
任务的返回值为:3
你也许会奇怪,我们的任务是通过Cancel的方式处理,为什么完成的状态IsCanceled那一栏还是False。这是因为在工作任务中,我们对于IsCancellationRequested进行了业务逻辑上的处理,并没有通过ThrowIfCancellationRequested方法进行处理。如果采用后者的方式,如下:
- static void Main(string[] args)
- {
- CancellationTokenSource cts = new CancellationTokenSource();
- Task<int> t =new Task<int>(() => AddCancleByThrow(cts.Token), cts.Token);
- t.Start();
- t.ContinueWith(TaskEndedByCatch);
- //等待按下任意一个键取消任务
- Console.ReadKey();
- cts.Cancel();
- Console.ReadKey();
- }
-
- static void TaskEndedByCatch(Task<int> task)
- {
- Console.WriteLine("任务完成,完成时候的状态为:");
- Console.WriteLine("IsCanceled={0}\tIsCompleted={1}\tIsFaulted={2}",
- task.IsCanceled, task.IsCompleted, task.IsFaulted);
- try
- {
- Console.WriteLine("任务的返回值为:{0}", task.Result);
- }
- catch (AggregateException e)
- {
- e.Handle((err) => err is OperationCanceledException);
- }
- }
-
- static int AddCancleByThrow(CancellationToken ct)
- {
- Console.WriteLine("任务开始……");
- int result = 0;
- while (true)
- {
- ct.ThrowIfCancellationRequested();
- result++;
- Thread.Sleep(1000);
- }
- return result;
- }

那么输出为:
任务开始……
任务完成,完成时候的状态为:
IsCanceled=True IsCompleted=True IsFaulted=False
在任务结束求值的方法TaskEndedByCatch中,如果任务是通过 ThrowIfCancellationRequested 方法结束的,对任务求结果值将会抛出异常 OperationCanceledException,而不是得到抛出异常前的结果值。这意味着任务是通过异常的方式被取消掉的,所以可以注意到上面代码的输出中,状态 IsCancled 为True。
再一次,我们注意到取消是通过异常的方式实现的,而表示任务中发生了异常的IsFaulted状态却还是等于False。这是因为 ThrowIfCancellationRequested 是协作式取消方式类型 CancellationTokenSource 的一个方法,CLR进行了特殊的处理。CLR知道这一行程序开发者有意为之的代码,所以不把它看作是一个异常(它被理解为取消)。要得到 IsFaulted 等于 True 的状态,我们可以修改 While 循环,模拟一个异常出来:
- while (true)
- {
- //ct.ThrowIfCancellationRequested();
- if (result == 5)
- {
- thrownew Exception("error");
- }
- result++;
- Thread.Sleep(1000);
- }
模拟异常后的输出为:
任务开始……
任务完成,完成时候的状态为:
IsCanceled=False IsCompleted=True IsFaulted=True
【任务工厂】
Task还支持任务工厂的概念。任务工厂支持多个任务之间共享相同的状态,如取消类型CancellationTokenSource就是可以被共享的。通过使用任务工厂,可以同时取消一组任务:
- static void Main(string[] args)
- {
- CancellationTokenSource cts = new CancellationTokenSource();
- //等待按下任意一个键取消任务
- TaskFactory taskFactory = new TaskFactory();
- Task[] tasks =new Task[]
- {
- taskFactory.StartNew(() => Add(cts.Token)),
- taskFactory.StartNew(() => Add(cts.Token)),
- taskFactory.StartNew(() => Add(cts.Token))
- };
- //CancellationToken.None指示TasksEnded不能被取消
- taskFactory.ContinueWhenAll(tasks, TasksEnded, CancellationToken.None);
- Console.ReadKey();
- cts.Cancel();
- Console.ReadKey();
- }
-
- static void TasksEnded(Task[] tasks)
- {
- Console.WriteLine("所有任务已完成!");
- }

以上代码输出为:
任务开始……
任务开始……
任务开始……
所有任务已完成(取消)!
上面演示了Task(任务)和TaskFactory(任务工厂)的使用方法。Task甚至进一步优化了后台线程池的调度,加快了线程的处理速度。在FCL4.0时代,使用多线程,我们理应更多地使用Task。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。