任务,基于线程池。其使咱们对并行编程变得更简单,且不用关心底层是怎么实现的。
System.Threading.Tasks.Task类是Task Programming Library(TPL)中最核心的一个类。html
1:任务是架构在线程之上的,也就是说任务最终仍是要抛给线程去执行。spring
2:任务跟线程不是一对一的关系,好比开10个任务并非说会开10个线程,这一点任务有点相似线程池,可是任务相比线程池有很小的开销和精确的控制。编程
咱们用VS里面的“并行任务”看一看,快捷键Ctrl+D,K,或者找到“调试"->"窗口“->"并行任务“,咱们在WaitAll方法处插入一个断点,最终咱们发现任务确实托管给了线程。api
两种构建Task的方式,只是StartNew方法直接构建出了一个Task以后又调用了其Start方法。安全
Task.Factory.StartNew (() => { Console.WriteLine("Hello word!"); }); Task task =new Task
(() => { Console.WriteLine("Hello,Word!"); }); task.Start();
在Task内部执行的内容咱们称做为Task的Body,Task提供了多个初始化重载的方法。架构
public Task(Action action); public Task(Action<object> action,object state
);给action传参数 public Task(Action action, CancellationToken cancellationToken); public Task(Action action, TaskCreationOptions creationOptions);
例如使用了重载方法的State参数:函数
Task task2 = new Task((obj ) =>
{ Console.WriteLine("Message: {0}", obj); },"Say \"Hello\" from task2
"); task2.Start();
补充细节oop
在建立Task的时候,Task有不少的构造函数的重载,一个主要的重载就是传入TaskCreateOptions的枚举: spa
任务结束时,它能够把一些有用的状态信总写到共享对象中。这个共享对象必须是线程安全的。
另外一个方式是使用返回某个结果的任务。使用Task类的泛型版本,就能够定义返冋某个结果的任务的返回类型。线程
使用返回值的Result属性可获取是在一个Task运行完成才会获取的,因此task2是在task1运行完成后,才开始运行,也就是说上面的两个result的值无论运行多少次都是不会变的。其中咱们也能够经过CurrentId来获取当前运行的Task的编号。
var loop = 0; var task1 = new Task<int>(() => { for (var i = 0; i < 1000; i++) loop += i; return loop; }); task1.Start(); var loopResut = task1.Result; var task2 = new Task<long>(obj=> { long res = 0; var looptimes = (int)obj; for (var i = 0; i < looptimes; i++) res += i; return res; },loopResut); task2.Start(); var resultTask2 = task2.Result; Console.WriteLine("任务1的结果':{0}\n任务2的结果:{1}", loopResut,resultTask2);
在 .NET Framework 4.5 及更高版本(包括 .NET Core 和 .NET Standard)中,使用静态 Task.Run 方法做为 TaskFactory.StartNew 的快捷方式。
Task.Run的跟Task.Factory.StarNew和new Task相差很少,不一样的是前两种是放进线程池当即执行,而Task.Run则是等线程池空闲后在执行。
Run方法只接受无参的Action和Func委托,另外两个接受一个object类型的参数。
在msdn中TaskFactory.StartNew的备注信息以下:
所谓的延续的Task就是在第一个Task完成后自动启动下一个Task。咱们经过ContinueWith方法来建立延续的Task。咱们假设有一个接受xml解析的服务,首先从某个地方接受文件,而后解析入库,最后发送是否解析正确的回执。在每次调用ContinueWith方法时,每次会把上次Task的引用传入进来,以便检测上次Task的状态,好比咱们可使用上次Task的Result属性来获取返回值。
var ReceiveTask = new Task(() => ReceiveXml()); var ResolveTask = ReceiveTask .ContinueWith <bool>((r) => ResolveXml()); var SendFeedBackTask = ResolveTask.ContinueWith <string>((s) => SendFeedBack(s.Result)); ReceiveTask.Start(); Console.WriteLine(SendFeedBackTask.Result);
上面的代码咱们也能够这么写:
var SendFeedBackTask = Task.Factory.StartNew(() => ReceiveXml()) .ContinueWith<bool>(s => ResolveXml()) .ContinueWith<string>(r => SendFeedBack(r.Result)); Console.WriteLine(SendFeedBackTask.Result);
不管前一个任务是如何结束的,前面的连续任务老是在前一个任务结束时启动。使用 TaskContinuationOptions枚举中的值,能够指定,连续任务只有在起始任务成功(或失败)结束吋启动。可能的值是 OnlyOnFaulted、NotOoFaulted、Onl)OnCanceIed、NotOnCanceled 和 OnlyOnRanToCompletion
Task t5 = t1.ContinueWith(DoOnError,
TaskContinuationOptions.OnlyOnFaulted);
有些状况下咱们须要建立嵌套的Task,嵌套里面又分为分离的和不分离的。其建立的方式很简单,就是在Task的body里面建立一个新的Task。若是新的Task未指定AttachedToParent选项,那么就是分离嵌套的。咱们看下面这段代码。下面的代码中outTask.Wait()表示等待outTask执行完成。
var outTask = Task.Factory.StartNew(() => { Console.WriteLine("Outer task beginning..."); var childTask = Task.Factory.StartNew(() => { Thread.SpinWait(3000000); Console.WriteLine("Detached nested task completed."); }); }); outTask.Wait(); Console.WriteLine("Outer task completed."); Console.ReadKey();
咱们能够看到运行结果是:
咱们将上面的代码加上TaskCreationOptions选项:
若是父任务在子任务以前结束,父任务的状态就显示为WaitingForChildrenToComplete。只要子任务也结束时,父任务的状态就变成RanToCompletion。.、固然,若是父任务用TaskCreatiooOptions 枚举中的DetachedFromParent建立子任务时,这就无效。
var outTask = Task.Factory.StartNew(() => { Console.WriteLine("Outer task beginning..."); var childTask = Task.Factory.StartNew(() => { Thread.SpinWait(3000000); Console.WriteLine("Detached nested task completed."); },TaskCreationOptions.AttachedToParent); }); outTask.Wait(); Console.WriteLine("Outer task completed.");
看到运行结果:
在4.0中给咱们提供一个“取消标记”叫作CancellationTokenSource.Token,在建立task的时候传入此参数,就能够将主线程和任务相关联。咱们经过cancellation的tokens来取消一个Task。
有点要特别注意的,当咱们调用了Cancel()方法以后,.NET Framework不会强制性的去关闭运行的Task。咱们本身必须去检测以前在建立Task时候传入的那个CancellationToken。
一旦cancel被调用,task将会抛出OperationCanceledException来中断此任务的执行,最后将当前task的Status的IsCanceled属性设为true。
一、在不少Task的Body里面包含循环,咱们能够在轮询的时候判断IsCancellationRequested属性是否为True,若是是True的话,就能够中止循环以及释放资源,同时抛出OperationCanceledException异常出来。
二、或者在任务中设置“取消信号“叫作ThrowIfCancellationRequested,来等待主线程使用Cancel来通知。
三、检测task是否被cancel就是调用CancellationToken.WaitHandle属性。CancellationToken的WaitOne()方法会阻止task的运行,只有CancellationToken的cancel()方法被调用后,这种阻止才会释放。
var cts = new CancellationTokenSource();
var ct = cts.Token;
var task = Task.Factory.StartNew(() =>
{
for (var i = 0; i < 10000000; i++)
{
if (ct.IsCancellationRequested)
{
Console.WriteLine("任务开始取消...");
throw new OperationCanceledException(ct);
}
//或者直接在检测到异常时,扔出异常: token.ThrowIfCancellationRequested();
//或者等待 WaitHandle: token.WaitHandle.WaitOne();
}
},ct);//传入CancellationToken做为Task第二个参数
ct.Register(() =>
{
Console.WriteLine("已经取消");
});
Thread.Sleep(5000);
cts.Cancel();//若是想要取消一个Task的运行,只要调用CancellationToken实例的Cancel()方法就能够了。
try
{
task.Wait();
}
catch (AggregateException e)
{
foreach (var v in e.InnerExceptions)
Console.WriteLine("msg: " + v.Message);
}
在TPL中咱们能够经过三种方式进行等待,一是经过CancellTaken的WaitHanle进行等待、第二种则是经过传统的Tread.Sleep方法、第三种则经过Thread.SpainWait方法。
一、CancellToken方式:每次咱们等待十秒钟以后,再进行下次输出。
有一点要注意:WaitOne()方法只有在设定的时间间隔到了,或者Cancel方法被调用,此时task才会被唤醒。若是若是cancel()方法被调用而致使task被唤醒,那么CancellationToken.WaitHandle.WaitOne()方法就会返回true,若是是由于设定的时间到了而致使task唤醒,那么CancellationToken.WaitHandle.WaitOne()方法返回false。
var cts = new CancellationTokenSource(); var ct = cts.Token; var task = new Task(() => { for (var i = 0; i < 100000; i++) { var cancelled = ct.WaitHandle.WaitOne(1000 ); Console.WriteLine(" {0}. Cancelled? {1}", i, cancelled); if (cancelled) { throw new OperationCanceledException(ct); } } }, ct); task.Start();
二、上面的功能若是咱们要是经过Tread.Sleep方式实现:
var task = new Task(() => { for (var i = 0; i < 100000; i++) { Thread.Sleep(10000); var cancelled =ct.IsCancellationRequested; Console.WriteLine(" {0}. Cancelled? {1}", i, cancelled); if (cancelled) { throw new OperationCanceledException(ct); } } },ct);
三、Thread.SpainWait则跟上面两种方式彻底不一样,上面的两种方式都是会在线程调度程序不考虑改线程,直等到运行结束。而Thread.SpainWait的做用实质上会将处理器置于十分紧密的循环中,主要的做用是来实现同步锁的做用。并不经常使用,大部分状况下咱们能够经过Lock的方式来实现。
Thread.SpinWait(10000);
在不少时候咱们也许须要等待同时开启的几个线程完成以后再来作其余事,在TPL中提供了几种方式来等待任务执行。Task.Wait等待单个任务完成;Task.WaitAll等待全部的Task完成、TaskAny等在其中的任何一个或则多个任务完成。
共有5个重载:Wait()、Wait(CancellToken)、Wait(Int32)、Wait(TimeSpan)、Wait(TimeSpan、CancellToken)。各个重载方法的含义:
static void Main(string[] args) { var tokenSource = new CancellationTokenSource(); CancellationToken token = tokenSource.Token; Task task = createTask(token,6); task.Start(); Console.WriteLine("Wait() complete."); task.Wait(); Console.WriteLine("Task Completed."); task = createTask(token,3); task.Start(); Console.WriteLine("Wait(2) secs for task to complete."); bool completed = task.Wait(2000); Console.WriteLine("Wait ended - task completed: {0}", completed); task = createTask(token,4); task.Start(); Console.WriteLine("Wait(2,token) for task to complete."); completed = task.Wait(2000, token); Console.WriteLine("Wait ended - task completed: {0} task cancelled {1}", completed, task.IsCanceled); Console.WriteLine("Main method complete. Press enter to finish."); Console.ReadLine(); } static Task createTask(CancellationToken token,int loop) { return new Task(() => { for (int i = 0; i < loop; i++) { token.ThrowIfCancellationRequested(); Console.WriteLine("Task - Int value {0}", i); token.WaitHandle.WaitOne(1000); } }, token); }
循环都会等待1秒钟,这样咱们能够看看Wait(2000)的效果,看看运行后的效果:
从上面的例子能够看出,wait方法子task执行完成以后会返回true。
注意:当在执行的task内部抛出了异常以后,这个异常在调用wait方法时会被再次抛出。后面再"异常处理篇"会讲述。
是等待全部的任务完成,也有5个重载, 也能够传递时间以及Token参数,进行等待时间以及取消Token的控制。
var tokenSource = new CancellationTokenSource(); CancellationToken token = tokenSource.Token; var task1 = createTask(token,2); var task2 = createTask(token, 5); task1.Start(); task2.Start(); Console.WriteLine("Waiting for tasks to complete."); Task.WaitAll(task1, task2); Console.WriteLine("Tasks Completed.");
注意:若是在等在的多个task之中,有一个task抛出了异常,那么调用WaitAll()方法时就会抛出异常。
ContinueWith结合WaitAll来玩一把
当这二者结合起来,咱们就能够玩一些复杂一点的东西,好比说如今有4个任务,其中t1须要串行,t2-t3能够并行,t4须要串行.
ConcurrentStack<int> stack = new ConcurrentStack<int>(); //t1先执行 var t1 = Task.Factory.StartNew(() => { stack.Push(1); stack.Push(2); }); //t2,t3并行执行 var t2 = t1.ContinueWith
(t => { int result; stack.TryPop(out result); }); //t2,t3并行执行 var t3 = t1.ContinueWith
(t => { int result; stack.TryPop(out result); }); //等待t2和t3执行完 Task.WaitAll(t2, t3); //t4z再执行 var t4 = Task.Factory.StartNew(() => { Console.WriteLine("当前集合元素个数:" + stack.Count); });
等待任何一个任务完成,完成以后返回其完成的任务的Index:
var tokenSource = new CancellationTokenSource(); CancellationToken token = tokenSource.Token; var task1 = createTask(token,2); var task2 = createTask(token, 5); task1.Start(); task2.Start(); Console.WriteLine("Waiting for tasks to complete."); varindex
= Task.WaitAny(task1, task2); Console.WriteLine("Tasks Completed.Index is {0}",index);
在TPL中,异常的触发器主要是这几个:
Task.Wait(), Task.WaitAll(), Task,WaitAny(),Task.Result。而在TPL出现的异常都会以AggregateException的示例抛出,咱们在进行基本的异常处理时,能够经过查看AggregateException的InnerExceptions来进行内部异常的捕获:
var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token; var task1 = new Task(() => { throw new NullReferenceException() {Source
="task1"}; }); var task2 = new Task(() => { throw new ArgumentNullException("a", "a para can not be null") { Source="task2"}; }); task1.Start(); task2.Start(); try { Task.WaitAll(task1, task2); } catch(AggregateException ex) { foreach (Exception inner in ex.InnerExceptions) { Console.WriteLine("Exception type {0} from {1}", inner.GetType(), inner.Source); } }
同时,咱们还能够经过Task的几个属性来判断Task的状态,如:IsCompleted, IsFaulted, IsCancelled,Exception。
另外,AggregateException中还提供了Handle方法来给咱们方法来给咱们处理每一个内部 异常,每一个异常发生时都会调用Handle传入的delegate ,同时咱们须要经过返回True,False来告诉异常是否已经被处理,好比对于OperationCanceledException咱们知道是取消了Task,是确定能够处理的:
try { Task.WaitAll(task1, task2, task3, task4); } catch(AggregateException ex) { ex.Handle((e) => { if (e is OperationCanceledException) { return true; } else { return false; } }); }
晚加载,或者又名延迟初始化,主要的好处就是避免没必要要的系统开销。在并行编程中,能够联合使用Lazy变量和Task<>.Factory.StartNew()作到这点。(Lazy变量时.NET 4中的一个新特性,这里你们不用知道Lazy的具体细节)。
Lazy变量只有在用到的时候才会被初始化。因此咱们能够把Lazy变量和task的建立结合:只有这个task要被执行的时候才去初始化。
// do the same thing in a single statement Lazy<Task<string>> lazyData2 = new Lazy<Task<string>>( () => Task<string>.Factory.StartNew(() => { Console.WriteLine("Task body working..."); return "Task Result"; })); Console.WriteLine("Calling second lazy variable"); Console.WriteLine("Result from task: {0}", lazyData2.Value.Result);
首先咱们回想一下,在以前的系列文章中咱们是怎么定义一个task的:直接new,或者经过task的factory来建立,由于建立task的代码是在main函数中的,因此只要new了一个task,那么这个task就被初始化。如今若是用了Lazy的task,那么如今咱们初始化的就是那个Lazy变量了,而没有初始化task,(初始化Lazy变量的开销小于初始化task),只有当调用了lazyData.Value时,Lazy变量中包含的那个task才会初始化。(这里欢迎你们提出本身的理解)