Task
通过ThreadPool.QueueUserWorkItem发起的异步线程存在局限性;
- 异步计算完成后没有返回值
- 主线程不知道异步计算何时完成
FCL提供了Task类型,提供了更加丰富计算限制的异步操作,全部是在线程池中创建的线程。
//以下两个函数效果等同与ThreadPool.QueueUserWorkItem
new Task(Dowork,paramter).Start();
Task.Run(()=>Dowork(paramter));
Task创建新的线程方式都提供了重载,可以传递一个CancellationToken实例。
等待任务完成
var task=new Task<bool>(Dowork);
task.Start();
task.Wait();//这里会引发当前线程的阻塞,当Dowork执行完成之后,才会继续执行下面的代码
Console.WriteLine($"Dowork执行结束了,结果是:"+task.Result)
如果计算限制的任务抛出未处理的异常,异常会被“吞噬”并存储到一个集合中,而线程可以返回到线程池中。调用Wait方法或者Result属性时,会抛出一个System.AggregateException对象
System.AggregateException类型封装了异常对象的一个集合(如果父任务生成了多个子任务,而多个子任务都抛出了异常,这个集合便可能包含多个异常)
Task类还提供了两个静态方法,允许线程等待一个Task对象数组:
WaitAny方法,阻塞调用线程,直到数组中的任何Task对象完成。返回数组索引,指明完成的是哪个Task对象。
WaitAll方法,阻塞调用线程,直到数组中所有Task对象完成。如果所有Task对象都完成,返回True;超时返回False.
WaitAny和WaitAll如果通过一个Cancellation取消,会抛出一个OperationCanceledException。
取消任务
用一个CancellationTokenSource取消Task,需要修改方法,让他接受一个CancellationToken
private static int Sum(CancellationToken ct, int n)
{
int sum = 0;
for(;n>0;n--)
{
//ct.ThrowIfCancellationRequested();
//任务有办法表示完成,任务甚至能返回一个值 所以需要采取一种方式将已完成的任务和出错的任务分开, 这就是一种方式
if (ct.IsCancellationRequested) break; //执行结果输出sum = 0
checked { sum += n; }
}
return sum;
}
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(state=>Console.WriteLine("123232323"));
CancellationTokenSource cts = new CancellationTokenSource();
Task<int> t = new Task<int>(n => Sum(cts.Token,(int)n),1000000);
t.Start();
cts.Cancel();
try
{
Console.WriteLine("The sum is:" + t.Result);
}
catch (AggregateException x)
{
x.Handle(e => e is OperationCanceledException);
Console.WriteLine("Sum is canceled");
}
Console.Read();
}
如果任务被取消 ,ThrowIfCancellationRequested会抛出一个OperationCanceledException(选择抛出异常是为了区分已完成的任务和出错的任务)
任务完成时自动启动新任务
伸缩性好的软件不应该使得线程阻塞,调用wait,或者在任务尚未完成时查询任务的Result属性,不利于性能和伸缩性。
幸好,有更好的办法可以知道一个任务在什么时候结束运行:任务完成时可启用另一个任务
Task<int> t = new Task<int>(n => Sum(cts.Token,(int)n),1000);
Task cwt = t.ContinueWith(task => Console.WriteLine("The sum is:" + t.Result));
如果在调用ContinueWith之前,任务已经完成,那么ContinueWith方法看到任务已经完成会立即启用显示结果的任务
Task对象内部包含了ContinueWith任务的一个集合,所以,实际可以用一个Task对象来多次调用ContinueWith。任务完成时,所有ContinueWith任务都会进入线程池的队列中。
任务可以启动子任务
任务支持父子关系
Task<int[]> parent = new Task<int[]>(() =>
{
var results = new int[3];//创建一个数组来存储结果
//这个任务创建并启动3个子任务
new Task(() => results[0] = Sum(cts.Token, 1000), TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[1] = Sum(cts.Token, 2000), TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[2] = Sum(cts.Token, 3000), TaskCreationOptions.AttachedToParent).Start();
//返回对数组的引用(即使数组元素可能还没有初始化)
return results;
});
var cwt2 = parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));
parent.Start();
TaskCreationOptions.AttachedToParent标志将一个Task和创建他的Task关联,除非所有的子任务(以及子任务的子任务)结束运行,否则创建任务(父任务)不认为已经结束
一个任务创建的一个或多个Task对象默认是顶级任务,与创建它们的任务无关。
任务内部揭秘
每个Task对象都有一组字段,字段构成了任务的状态。包括ID、代表Task执行状态的Int32、对父任务的引用、对Task创建时指定的TaskSchedule的引用、对回调方法的引用等。
任务的创建是有代价的,必须为所有状态分配内存。如果不需要任务的附加功能,那么使用ThreadPool.QueueUserWorkItem能获得更好的资源利用率。
在一个Task对象的存在期间,可查询Task的只读Status属性了解它在生存期的什么位置
public enum TaskStatus
{
// 该任务已初始化,但尚未被计划。
Created = 0,
// 该任务正在等待 .NET Framework 基础结构在内部将其激活并进行计划。
WaitingForActivation = 1,
// 该任务已被计划执行,但尚未开始执行。 ContinueWith,ContinueWhenAll,ContinueWhenAny,FromAsync
WaitingToRun = 2,
// 该任务正在运行,但尚未完成。
Running = 3,
// 该任务已完成执行,正在隐式等待附加的子任务完成。
WaitingForChildrenToComplete = 4,
// 已成功完成执行的任务。
RanToCompletion = 5,
// 该任务已通过对其自身的 CancellationToken 引发 OperationCanceledException 对取消进行了确认,此时该标记处于已发送信号状态;或者在该任务开始执行之前,已向该任务的
// CancellationToken 发出了信号。有关详细信息,请参阅任务取消。
Canceled = 6,
// 由于未处理异常的原因而完成的任务。
Faulted = 7
}
为了简化编码,Task提供了几个属性 IsCanceled,IsFaulted和IsCompleted
任务处于 RanToCompletion,Canceled,Faulted状态是,IsCompleted返回True。
判断一个任务是否成功完成最简单的方式是
if(task.Status == TaskStatus.RanToCompletion)
任务工厂
有时需要创建一组共享相同配置的Task对象。
为了避免重复的将相同的参数传给每个Task的构造器,可创建一个任务工厂来封装通用的配置。
System.Threading.Tasks 定义了
TaskFactory 用于创建返回void的任务
TaskFactory 用于创建返回特定返回类型的任务
Task parent2 = new Task(() =>
{
var cts = new CancellationTokenSource();
var tf = new TaskFactory<int>(
cts.Token,
TaskCreationOptions.AttachedToParent,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default
);
var childTasks = new[]{
tf.StartNew(()=> Sum(cts.Token,1000)),
tf.StartNew(()=> Sum(cts.Token,2000)),
tf.StartNew(()=> Sum(cts.Token,3000))
};
});
任务调度器
TaskSchedule对象负责执行被调度的任务,同时向Visual Studio调试器公开任务信息。FCL提供两个派生自TaskSchedule的类型:线程任务调度器和同步上下文调度器。默认情况下,所有应用程序使用的都是线程任务调度器。可以查询TaskSchedule的静态Default属性来获得对默认任务调度器的引用
同步上下文调度器适合提供了图形界面的应用程序,例如Windows窗体、WPF、Windows Store应用程序。它将所有的任务都调度给应用程序的GUI线程,使所有任务代码都能成功更新UI组件。