线程(Thread、ThreadPool)
线程的定义我想大家都有所了解,这里我就不再复述了。我这里主要介绍.NET Framework中的线程(Thread、ThreadPool)。
.NET Framework中的线程分为两类:1.前台线程;2.后台线程。
1.前台线程
class Program { static void Main(string[] args) { Console.WriteLine("=====Thread====="); TestThread(); Console.WriteLine("主线程执行完毕"); } public static void TestThread() { Thread thread = new Thread(PrintNum); thread.Start(); } public static void PrintNum() { Thread.Sleep(3000); for (int i = 0; i < 10; i++) Console.WriteLine(i); } }
运行结果
从运行结果可以看出,主线程虽然执行完毕了,但是并没有退出程序,而是等待子线程执行完毕后,退出程序。
2.后台线程
class Program { static void Main(string[] args) { Console.WriteLine("=====ThreadPool====="); ThreadPool.QueueUserWorkItem(new WaitCallback(PrintNum)); Console.WriteLine("主线程执行完毕"); } public static void PrintNum(object obj) { Thread.Sleep(3000); for (int i = 0; i < 10; i++) Console.WriteLine(i); } }
运行结果
从运行结果可以看出,主线程运行完毕后,就直接退出了程序,没有等待子线程。
总结:
1.前台线程:主线程执行完毕后,会等待所有子线程执行完毕后,才退出程序。
2.后台线程:主线程执行完毕后,直接退出程序,不论子线程是否执行完毕。
3.推荐:多线程的操作,推荐使用线程池线程而非新建线程。因为就算只是单纯的新建一个线程,这个线程什么事情也不做,都大约需要1M的内存空间来存储执行上下文数据结构,并且线程的创建与回收也需要消耗资源,耗费时间。而线程池的优势在于线程池中的线程是根据需要创建与销毁,是最优的存在。但是这也有个问题,那就是线程池线程都是后台线程,主线程执行完毕后,不会等待后台线程而直接结束程序。所以下面就要引出.NET Framework4.0提供的Task,来解决此类问题。
Task
Task是.NET Framework4.0提供的新的操作线程池线程的封装类。它提供:等待、终止、返回值...优化线程操作的功能。
1.定义
Task 对象是一种的中心思想 基于任务的异步编程模式 首次引入.NET Framework 4 中。 因为由执行工作 Task 对象通常上异步执行一个线程池线程而不是以同步方式在主应用程序线程中,您可以使用 Status 属性,以及 IsCanceled, ,IsCompleted, ,和 IsFaulted 属性,以此来确定任务的状态。
以上MSDN中对Task的定义,从“异步执行一个线程池线程”可以得出Task的后台实现是通过线程池线程实现。
2.补充
Task的性能要优于ThreadPool。
1)ThreadPool的代码将以先进先出的算法存储在全局队列中,并且多个工作者线程之间竞争同一个同步锁。(这就Task性能优于ThreadPool的第一个原因)
2)Task的代码将以先进后出的算法存储在本地队列中,工作者线程执行本地队列中的代码没有同步锁的限制(这是Task性能优于ThreadPool的第二个原因),并且当工作者线程2空闲并且工作者线程1忙碌时,工作者线程2会尝试从工作者线程1(或者别的忙碌的工作者线程)的本地队列尾部“偷”任务,并会获取一个同步锁,不过这种行为很少发生。
3)简单调用
class Program { static void Main(string[] args) { TestSimpleTask(); } public static void TestSimpleTask() { Console.WriteLine("=====Task====="); //直接创建 Task task2 = new Task(() => { Thread.Sleep(3000); for (int i = 0; i < 10; i++) { Console.WriteLine(i); } }); //如果你想测试超时等待后,任务是否会继续执行。就替换下面的代码 task2.Start(); task2.Wait(); Console.WriteLine("主程序执行完毕"); /*测试超时 task2.Start(); task2.Wait(1000); Console.WriteLine("主程序执行完毕"); Console.ReadLine(); */ } }
task.Wati(时间);这个方法可以确定等待任务的执行时间,当超过规定的时间后将不再等待,直接运行之后的代码,但是任务的代码仍然在后台运行,如果想超过等待时间就停止任务的执行,你需要看下文深入学习。(这个方法提供了极大的方便。如果在.NET Framework2.0中,实现类似的功能,需要定义一个时间全局变量,然后在线程中不断的循环判断。)
3.深入学习
Task(Action, CancellationToken, TaskCreationOptions)
以上是MSDN中Task(不包含输入参数与返回值)最复杂的构造函数,包含2个重要的参数CancellationToken、TaskCreationOptions,下面将详细介绍CancellationToken、TaskCreationOptions的意义以及运用。
a.CancellationToken(取消标记)
该类用来检测任务是否被取消。需要与System.Threading.CancellationTokenSource配合使用,CancellationTokenSource主动停止任务。(CancellationToken虽然有检测任务是否停止的属性,但是一旦CancellationTokenSource调用了Cancel()方法,那么任务将立即停止运行,也就是说任务中的任何代码都不会被执行)
场景:主线程抛出异常,在异常处理中停止任务的执行。
class Program { static void Main(string[] args) { TestCancellationTokenTask(); } public static void TestCancellationTokenTask() { CancellationTokenSource cts = new CancellationTokenSource(); try { Task task = Task.Factory.StartNew(() => { for (int i = 0; i < 10; i++) { //当任务取消时,这段检测代码将永远不会被执行,因为任务已经被取消了 if (cts.Token.IsCancellationRequested) { Console.WriteLine("=====Task====="); Console.WriteLine("任务被取消"); break; } else { Console.WriteLine("=====Task====="); Console.WriteLine("子线程打印:{0}", i); Thread.Sleep(1000); } } }, cts.Token); for (int i = 0; i < 5; i++) { if (i == 3) { Console.WriteLine("=====Main====="); Console.WriteLine("主线程抛出异常"); throw new Exception("测试"); } Console.WriteLine("=====Main====="); Console.WriteLine("主线程打印:{0}", i); Thread.Sleep(1000); } task.Wait(); } catch { cts.Cancel(); } Console.WriteLine(cts.IsCancellationRequested); } }
注意:主线程抛出异常,无论任务是否被显示取消,都会停止运行。
b.TaskCreationOptions(任务创建选项)
以下是MSDN中关于TaskCreationOptions的枚举值,具体的运用还是要根据实际情况。下面介绍一下AttachedToParent的用法(第5、第6,实际的运用还需要多参考大神的运用)
// 默认
1.None
// 将任务放入全局队列中(任务将以先到先出的原则被执行)
2.PreferFairness
// 告诉TaskScheduler,线程可能要“长时间运行”
3.LongRunning
// 将一个Task与它的父Task关联
4.AttachedToParent
// Task以分离的子任务执行
5.DenyChildAttach
// 创建任务的执行操作将被视为TaskScheduler.Default默认计划程序
6.HideScheduler
// 强制异步执行添加到当前任务的延续任务
7.RunContinuationsAsynchronously
场景:将多个任务关联为父子任务
class Program { static void Main(string[] args) { TestTaskCreationOptionsTask(); } public static void TestTaskCreationOptionsTask() { StringBuilder sb = new StringBuilder(); Task parent = new Task(() => { new Task(() => { sb.Append("任务1"); }).Start(); new Task(() => { sb.Append("任务2"); }).Start(); new Task(() => { sb.Append("任务3"); }).Start(); //parent任务的调用线程停止5s,这样“任务1”、“任务2”、“任务3”就有时间执行完毕了。 //这里用来测试,当任务彼此之间是独立的,那么只有这种方式,控制台才会有打印。 //Thread.Sleep(5000); }); /* Task parent = new Task(() => { new Task(() => { sb.Append("任务1"); }, TaskCreationOptions.AttachedToParent).Start(); new Task(() => { Thread.Sleep(3000); sb.Append("任务2"); }, TaskCreationOptions.AttachedToParent).Start(); new Task(() => { Thread.Sleep(3000); sb.Append("任务3"); }, TaskCreationOptions.AttachedToParent).Start(); }); */ parent.Start(); parent.Wait(); Console.WriteLine(sb.ToString()); } }
说明:
1.Task的创建如果没有TaskCreationOptions.AttachedToParent,那么任务彼此之间是独立的,parent任务不会等待“任务1”、“任务2”、“任务3”都执行完毕后,才认为已经结束。
2.Task的创建如果有TaskCreationOptions.AttachedToParent,那么父任务必须等待子任务都执行完毕后,才会认为任务结束。
注意:虽然“任务1”、“任务2”、“任务3”是在parent任务中创建,但是可以分配在不同的线程池本地队列中,由不同的线程调用并且任务间并不是串行执行,而是并行执行。
c.返回值
以上介绍的所有内容,Task都没有返回值,但是在实际运用中,Task执行完之后,返回一个值供外部代码使用,这种情况很常见。
class Program { static void Main(string[] args) { TestReturnValueTask(); } public static void TestReturnValueTask() { Task<int> task = new Task<int>(num => { Thread.Sleep(5000); return (int)num + 1; }, 100); task.Start(); Console.WriteLine(task.Result); } }
注意:当Task的返回值被调用时,主线程会等待Task执行完毕,才会退出程序。所以这里没有调用task.Wait();
d.TaskContinuationOptions(任务延续选项)
有时候,我们需要在一个任务结束后,执行另一个任务。两个任务之间有先后顺序,这个时候,就需要用到TaskContinuationOptions。
以下是MSDN中关于TaskContinuationOptions的枚举值,这里只列出了部分(4.5新增的枚举只根据MSDN的机器翻译确实不太理解如何运用,还是需要花些时间测试,这里就不列出了,怕误导读者)
// 默认
1.None
// 将任务放入全局队列中(任务将以先到先出的原则被执行)
2.PreferFairness
// 告诉TaskScheduler,线程可能要“长时间运行”,需要为任务创建一个专用线程,而不是排队让线程池线程来处理
3.LongRunning
// 将一个Task与它的父Task关联
4.AttachedToParent
// 希望执行第一个Task的线程,执行ContinueWith任务
5.ExecuteSynchronously
// 第一个任务没有完成,执行后续任务
6.NotOnRanToCompletion
// 第一个任务没有失败,执行后续任务
7.NotOnFaulted
// 第一个任务没有取消,执行后续任务
8.NotOnCanceled
// 只有当第一个任务取消,执行后续任务
9.OnlyOnCanceled
// 只有当第一个任务失败,执行后续任务
10.OnlyOnFaulted
// 只有当第一个任务完成,执行后续任务
11.OnlyOnRanToCompletion
class Program { static void Main(string[] args) { TestContinueTask(); } public static void TestContinueTask() { /* Task<int> task = new Task<int>(num => { return (int)num + 1; }, 100); Task taskContinue = task.ContinueWith(c => { Console.WriteLine(c.Result); }, TaskContinuationOptions.OnlyOnRanToCompletion); task.Start(); taskContinue.Wait(); */ CancellationTokenSource cts = new CancellationTokenSource(); Task<int> task = new Task<int>(num => { return (int)num + 1; }, 100, cts.Token); Task taskContinue = task.ContinueWith(c => { Console.WriteLine("任务Task被取消了"); }, TaskContinuationOptions.OnlyOnCanceled); task.Start(); cts.Cancel(); taskContinue.Wait(); } }
写到这里,关于Task的介绍已经结束。但是Task的内容还有很多:异常处理(AggregateException)、取消通知委托(CancellationToken的Register方法)这些内容就留给读者自己去学习了。
有时候,可能需要一次性创建多个任务,并且这些任务共享相同的状态。那么我们就可以通过任务工厂来创建。
TaskFactory
任务工厂,顾名思义,用来创建任务的工厂。在大多数情况下,不需要实例化一个新 TaskFactory<TResult> 实例。 可以使用静态Task<TResult>.Factory 属性,它返回一个工厂对象,将使用默认值。 然后可以调用其方法来启动新任务或定义任务延续。
代码
class Program { static void Main(string[] args) { TestTaskFactory(); } public static void TestTaskFactory() { TaskFactory<DateTime> factory = new TaskFactory<DateTime>(); Task<DateTime>[] tasks = new Task<DateTime>[] { factory.StartNew(() => { return DateTime.Now.ToUniversalTime(); }), factory.StartNew(() => { Thread.Sleep(5000); return DateTime.Now.ToUniversalTime(); }), factory.StartNew(() => { return DateTime.Now.ToUniversalTime(); }) }; StringBuilder sb = new StringBuilder(); foreach (Task<DateTime> task in tasks) sb.AppendFormat("{0}\t", task.Result); Console.WriteLine(sb.ToString()); } }
注意:任务可以分配在不同的线程池本地队列中,由不同的线程调用并且任务间并不是串行执行,而是并行执行。
Parallel
并行,让多个线程池线程并行工作。由于是并行执行,所以有一点需要注意:工作项彼此之间必须可以并行执行!
class Program { static void Main(string[] args) { TestParallel(); } public static void TestParallel() { Parallel.For(0, 10, i => { Console.WriteLine(i); }); List<int> lists = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; Parallel.ForEach(lists, i => { Console.WriteLine(i); }); } }
说明:
1.Parallel.For效率高于Parallel.Foreach,所以当For与Foreach都可以时,推荐使用For。
2.上面的代码,运行For时,你可能会发现数字是有顺序的打印出来,给人一种串行执行的错觉,你可以断点调试你的代码,会发现确实有多个线程在运行代码。
3.Parallel.For()、Parallel.Foreach()还有一些重载方法,大家可以结合实际情况使用,这里就不复述了。
感谢大家的耐心阅读。