一、简介
在4.0之前,多线程只能用Thread或者ThreadPool,而4.0下提供了功能强大的Task处理方式,这样免去了程序员自己维护线程池,而且可以申请取消线程等。。。所以本文主要描述Task的特性。
二、Task的优点
操作系统自身可以实现线程,并且提供了非托管的API来创建与管理这些线程。但是C#是运行在CLR上面的,为了方便的创建与管理线程,CLR对这些API进行了封装,通过System.Threading.Tasks.Task公开了这些包装。
在计算机中,创建线程十分耗费珍贵的计算机资源,所以Task启动时,不是直接创建一个线程。而是从线程池请求一个线程。并且通过对线程的抽象,程序员一般和Task打交道就好,这样降低了高效管理多线程的复杂度。
三、Task使用示例。
Task可以获取一个返回值,下面的程序实现如下功能:利用Task启动一个新的线程,然后计算3*5的值,并返回。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading.Tasks; 5 using System.Text; 6 7 namespace TaskTest 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 // 定义并启动一个线程,计算5乘以3,并返回一个int类型的值 14 Task<int> task = Task.Factory.StartNew<int>( 15 () => { return 5 * 3; }); 16 // 线程启动并开始执行 17 18 foreach (char busySymbol in Utility.BusySymbols()) 19 { 20 if (task.IsCompleted) 21 { 22 Console.Write('\b'); 23 break; 24 } 25 Console.Write(busySymbol); 26 } 27 Console.WriteLine(); 28 29 Console.WriteLine(task.Result.ToString()); 30 // 如果执行至此仍未完成那个线程,则输出堆栈信息 31 System.Diagnostics.Trace.Assert(task.IsCompleted); 32 } 33 } 34 public class Utility 35 { 36 public static IEnumerable<char> BusySymbols() 37 { 38 string busySymbols = @"-\|/-\|/"; 39 int next = 0; 40 { 41 while (true) 42 { 43 yield return busySymbols[next]; 44 next = (++next) % busySymbols.Length; 45 yield return '\b'; 46 } 47 } 48 } 49 } 50 }
输出结果如下两种:
可以看出, 第一次运行如左图。首次运行到if (task.IsCompleted)的时候,计算3*5个线程还没有执行完,所以直接执行:
Console.Write(busySymbol);
输出了“-”,第二次到if的时候,3*5计算完成,执行if里面的内容,输出换行,跳出。然后执行到 Console.WriteLine(task.Result.ToString()); 输出15
第二次运行如右图。首次运行到if的时候,3*5已经计算完成,所以只输出了一个空的换行。然后输出15。其中接收返回值的语句是:
Console.WriteLine(task.Result.ToString());
当然,Task还有一套start的方法,但是不常用,用Task的静态Factory属性的StartNes方法就可以实例化并启动一个线程了,而且,附带指定了返回值的类型。
四、ContinueWith
Task包含了一个Continue的方法,这个方法可以将多个任务连接起来,可以指定当前线程完成之后启动哪个或者哪些线程。ContinueWith会返回另外一个Task,所以工作链可以持续下去。
用法如下:
1 static void Main(string[] args) 2 { 3 Task<int> task = Task.Factory.StartNew<int>( 4 () => { return 3 * 5; }); 5 Task faultTask = task.ContinueWith( 6 (antecedentTask) => { 7 System.Diagnostics.Trace.Assert(task.IsFaulted); 8 Console.WriteLine("Task State:Faulted"); 9 },TaskContinuationOptions.OnlyOnFaulted); 10 Task canceledTask = task.ContinueWith( 11 (antecedentTask) => 12 { 13 System.Diagnostics.Trace.Assert(task.IsCanceled); 14 Console.WriteLine("Task State:Canceled"); 15 },TaskContinuationOptions.OnlyOnCanceled); 16 Task completedTask = task.ContinueWith( 17 (antecedentTask) => 18 { 19 System.Diagnostics.Trace.Assert(task.IsCompleted); 20 Console.WriteLine("Task State:Complete,Value is "+antecedentTask.Result.ToString()); 21 }, TaskContinuationOptions.OnlyOnRanToCompletion); 22 completedTask.Wait(); 23 }
ContinueWith的参数是一个与task(即后面任务的先驱任务的祖先)相同类型的Task参数。当启动后代任务时,自动将先驱任务赋值给ContinueWith的参数,所以本例输出结果是:
Task State:Complete,Value is 15.
如果我不使用completedTask.Wait();这一句,那么主线程完成后,不会去管task及其后续任务是否完成,就退出,所以加上了这几句话,这样避免task与后继任务执行完之前退出。这样就可以将任务连接回调用线程(main)了。当然要注意,这个例子中只有completeTask是存在的,因为task是正常执行的。不能用canceledTask.Wait(); 因为这个任务在task正常的情况下,永远不会被执行。
五、异常处理
当然也是用try-catch捕捉异常,但是在哪何时捕捉异常,都是一个问题。
从CLR2.0开始,在终结器线程、线程池线程和用户自己创建的线程中发生的未处理的异常一般会在异常层次结构中冒泡。如果冒泡到上一层,可以捕捉到这个异常,则十分好。Task支持这样一个机制;
即,Task在执行期间发生了未处理的异常,这个异常会被禁止(suppressed),抑制,线程后面不继续执行,标记为运行完成。直到调用某个任务完成成员例如:Wait(),Result,Task.WaitAll()或者Task.WaitAny(),才会重新引发线程执行期间未处理的异常,下面的代码展示了这样的机制:
1 static void Main(string[] args) 2 { 3 Task task = Task.Factory.StartNew( () => 4 { 5 throw new ApplicationException(); 6 Console.WriteLine("我之前有异常"); 7 }); // 显式抛出一个异常 8 try 9 { 10 task.Wait(); 11 } 12 catch (AggregateException ex) 13 { 14 foreach (Exception e in ex.InnerExceptions) 15 { 16 Console.WriteLine(e.Message); 17 } 18 } 19 }
从task线程里面抛出了异常,从wait()的时候捕捉到了异常。注意 catch (AggregateException ex)里面的参数是 AggregateException,这是一个异常集合。
当然,还有另外一种方法来处理这种异常。就是使用前文提到的ContinueWith认为,利用ContinueWith()中的task参数,可以评估先驱任务的Exception属性。代码如下:
1 #region ContinueWith触发先驱任务的异常 2 3 // 获取先驱任务是否失败的标志 4 bool parentTaskFaulted = false; 5 Task task = Task.Factory.StartNew(() => 6 { 7 throw new ApplicationException(); 8 }); 9 Task faultedTask = task.ContinueWith( (parentTask) => 10 { 11 parentTaskFaulted = parentTask.IsFaulted; 12 }); 13 faultedTask.Wait(); 14 15 // 如果 parentTaskFaulted = false,输出堆栈,即程序正常执行时显示 16 Trace.Assert(parentTaskFaulted); 17 18 // task无异常的情况下 19 if (!task.IsFaulted) 20 { 21 task.Wait(); 22 } 23 else 24 { 25 Console.WriteLine("ERROR" + task.Exception.Message); 26 } 27 #endregion
注意,并不是调用task.Wait()引发的异常,而是用faultedTask去检查task是否产生了异常。
六、取消任务
Task中取代了粗暴的kill和absort,而是设置了一个变量,然后主线程可以申请取消子线程,当线程收到取消信号时,会执行完当前的一次,然后取消。
代码如下:
1 static void Main(string[] args) 2 { 3 string start = "*".PadRight(Console.WindowWidth - 1, '*'); 4 Console.WriteLine("Push ENTER to exit."); 5 6 CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); 7 8 // 附加了一个token参数,是否取消的标志 9 Task task = Task.Factory.StartNew( 10 () => WriteChar(cancellationTokenSource.Token), cancellationTokenSource.Token); 11 // 等待输入任何一个字符 12 Console.ReadLine(); 13 // 请求取消 14 cancellationTokenSource.Cancel(); 15 Console.WriteLine(start); 16 task.Wait(); 17 Console.ReadLine(); 18 } 19 private static void WriteChar(CancellationToken cancellationToken) 20 { 21 int i = 0; 22 string charChain = string.Empty; 23 // 无取消请求的时候 24 while (!cancellationToken.IsCancellationRequested || i == int.MaxValue) 25 { 26 charChain += "tom"+i.ToString() + "\n"; 27 Console.WriteLine(charChain); 28 } 29 }
上述代码中,cancellationTokenSource.Cancel()与 task.wait();之间打印星号,我们在运行结果中可能会发现,在星号后面仍然输出了一个char,因为cancel后,线程不会马上终止,而是执行完当前的代码,然后直到下次判断是否终止后才终止。
不过这个例子中,WirteChar中的while循环太短暂,所以没有很好的展示出task可能的额外的一次执行。
七、多线程编程中,三种方法都可选的情况下,优先使用Task的方式,其次使用Threadpool,最次之使用Thread
参考资料: 《C#本质论》第三版第18章