作者:zlbcdn
现状
使用ThreadPool的QueueUserWorkItem方法完成异步操作会存在两个问题:
1、系统无法知道异步操作是否完成
2、无法获取异步操作完成时的返回值
问题来了,那就需要新的解决方案(忽然想起上《通信原理》时老师讲的话,“遇到问题,解决问题,因此就有了不同的编码方式”,从调幅,到调频,再到码分….,工程领域的主题就是遇到问题,解决问题!跑题了!)
为了解决上面提到的问题,.NET提出了Task的概念
Task
Task的构造方法如下图所示:
Task<TResult>
类,其就是代表一个可以有返回值的异步操作。 public delegate TResult Func<out TResult>()
具体的例子如下:
假设,操作的代码如下:
private static int Sum(int n){ int sum=0; for(;n>0;n--) checked{sum += n;} return sum;}
若程序希望获取操作的返回值,则应该如此使用:
Task<int> t = new Task<int>(n=>Sum((int)n),1000000);t.start();t.wait(); //可以写这句,也可以不写Console.WriteLine(t.Result);
在.NET中有两个Task类,一个是不返回参数的,其参数中使用的是Action委托
可返回结果的Task,其参数是使用的Func<Object,TResult>
或者是不带参数的Func<TResult>
Task的异常
在Task的异步操作中可能会出现异常,若是使用Task<TResult>
,在调用其Wait或Result方法时,异常就会抛出。若使用的Task
,则可以调用wait方法,异常就会抛出。另外,不管是Task<TResult>
还是Task
,只要查询其Exception属性,异常也会抛出。
Task的异常会被存储在一个异常集合中,其名称为AggregateException。其内部有一个InnerExceptions 的属性,其定义如下:
public ReadOnlyCollection<Exception> InnerExceptions { get; }
通过定义可以看到其返回结果是一个异常的集合类。因此,可以通过for循环,完成对每一个异常的处理
AggregateException还有一个Handle方法,该方法的定义如下:
public void Handle(Func<Exception,?bool> predicate)
(写到这儿,不得不发出感慨!当初设计.NET的这帮人真他妈厉害!)
这个方法的作用就是为AggregateException内包含的每一个异常都调用一个回调方法
举例说明:
using System;using System.IO;using System.Threading.Tasks;public class Example{ public static void Main() { // This should throw an UnauthorizedAccessException. try { var files = GetAllFiles(@"C:\"); if (files != null) foreach (var file in files) Console.WriteLine(file); } catch (AggregateException ae) { //这儿就是一个遍历InnerExceptions循环,将内部所有的异常全部遍历,然后进行相应处理 //下面的代码比较简单,可通过判断是个什么异常,从而进行下一步的相关操作 foreach (var ex in ae.InnerExceptions) //例如: //if(ex is UnauthorizedAccessException) // doSomeThing(); Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message); } Console.WriteLine(); // This should throw an ArgumentException. try { foreach (var s in GetAllFiles("")) Console.WriteLine(s); } catch (AggregateException ae) { foreach (var ex in ae.InnerExceptions) Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message); } } static string[] GetAllFiles(string path) { var task1 = Task.Run( () => Directory.GetFiles(path, "*.txt", SearchOption.AllDirectories)); try { return task1.Result; } catch (AggregateException ae) { //Handle是传入一个异常参数,返回一个bool的结果,若处理则返回true,否则返回false //Handle方法应该是一个遍历方法,即通过InnerExceptions属性,为每一个异常添加这个回调方法 ae.Handle( x => { // Handle an UnauthorizedAccessException if (x is UnauthorizedAccessException) { Console.WriteLine("You do not have permission to access all folders in this path."); Console.WriteLine("See your network administrator or try another path."); } return x is UnauthorizedAccessException; }); return Array.Empty<String>(); } }}
Task的方法关于阻塞
Task类中有几个Wait方法:Wait、WaitAny、WaitAll。具体可参考MSDN的方法说明:具体的链接在这儿,这儿说明的是:
不管是哪个方法,调用这三者方法的任何一个,都会造成调用线程被阻塞,即等待task完成相关的操作。
这儿就有一个区别,之前join方法与wait方法同样都是让线程等待,但是内部如何实现及两者的区别
.NET中join与wait方法的区别
在.NET中join方法的源码如下:(查看.NET的源码的网址在这儿:查看.NET的源码网址)
//Join方法的源代码[SecuritySafeCritical, HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]public void Join(){ this.JoinInternal();}
JoinInternal的源代码未知,只知是window内核的方法
[MethodImpl(MethodImplOptions.InternalCall), SecurityCritical, HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]private extern void JoinInternal();
而wait方法的代码如下:
//其他的无关代码,关键的一行如下:Thread.SpinWait(Environment.ProcessorCount * (((int) 4) << i));
而SpinWait是也是window内核的方法,但是通过名称可以得知,其是一个自旋的等待。即代表占用CPU的资源,使得线程被挂起。
个人对join方法的理解,如下图所示:
t.Wait()
语句时, 调用线程并不是就傻傻的被阻塞了,而是先去看看当前线程是否开始执行,若当前线程尚未开始执行操作,则调用线程就会将当前线程将要执行的操作加载到调用线程中执行;若当前线程的操作已经开始执行了,那没办法,调用线程只能被阻塞。 //构造一个取消操作对象private CancellationTokenSource cts = new CancellationSource();public static void Main(string[] args){ //定义一个具有返回结果类型的Task<TResult> Task<int> t=new Task<int>(tempMethod,t.Token); t.start(); //取消操作 cts.Cancel(); try { Console.WriteLine("返执行结果是:"+t.Result); } catch(AggregateException ex) //只要是使用了Task,则catch捕获的异常就应该使用AggregatException,而不是简单的使用Exception { ex.Handle(e=>e is OperationCanceledException); Console.WriteLine("Sum操作已经完成") }}//要符合Func委托private static int tempMethod(){ int resultInt=Sum(cts.Token,10000);}private static int Sum(CancellationToken ct,int n){ int sum=0; for(;n>0;n--) { //若请求操作取消操作,则抛出OperationCanceledException ct.ThrowIfCancellationRequest(); checked{sum+=n;} } return sum;}
当任务完成后,自动执行下一个任务
通过wait方法或者是task.Result属性获取操作最后的结果,这个是存在问题的。因为这样做会阻塞线程(Result属性的内部就是调用了wait方法)。因此为了避免因阻塞而导致的性能问题,.NET提供了一种回调机制,当线程完成操作时,就会调用callback方法。
特别说明,调用callback的线程是一个新的线程。
而实现callback的就是ContinueWith方法,ContinueWith方法的定义如下,更多的定义参见:MSDN中方法的定义
public Task ContinueWith( Action<Task, object> continuationAction, object state, CancellationToken cancellationToken, TaskContinuationOptions continuationOptions, TaskScheduler scheduler)
2、一个Task对象内部包含了一个ContinueWith的对象集合。即一个task对象可以声明多个继续的操作。例如:
Task t = new Task(Action()); t.ContinueWith(task=>Console.WriteLine("操作1:Task的结果是"+t.Result)); t.ContinueWith(task=>Console.WriteLine("操作2:Task的结果是"+t.Result)); t.ContinueWith(task=>Console.WriteLine("操作3:Task的结果是"+t.Result));
当Task的操作完成后,线程池中会启用3个线程处理相应的回调方法。
3、ContinueWith方法中有一参数:TaskContinuationOptions,是指存在某些情况的下才调用,具体参见参数。一般而言,continuewith是通过创建一个独立的task完成回调方法的调用,但是这个选项中也可以有一个AttachedToParent选项,使其成为一个Task的子任务
创建子任务
因为task的构造器中只是定义了委托的定义,但没有规定符合委托的方法中是否可以创建task。因此可以在task内部创建子任务,但是在创建子任务的时候,需要明确子任务的创建方式,若是正常创建,则新任务将是独立的task。想成为task的子任务,可以在task的构造器中使用TaskCreationOptions.AttachedToParent属性。这样就相当于子任务与父任务绑定在一起,只有当所有的任务完成时,父任务才认为是完成了。举例如下:
//在定义的时候同步创建了3个子线程,并且用了TaskCreationOptions.AttachedToParent属性Task<int[]> parent = new Task<int[]>( int[] results=new int[3]; new Task(()=>results[0]=Sum(100),TaskCreationOptions.AttachedToParent).start(); new Task(()=>results[0]=Sum(200),TaskCreationOptions.AttachedToParent).start(); new Task(()=>results[0]=Sum(300),TaskCreationOptions.AttachedToParent).start(); return results;);var ctw = parent.ContinueWith( parentTask=> Array.ForEach(Console.WriteLine("结果为:"+parentTask.Result)););parent.Start();
任务内部/小节
1、Task所使用的线程默认是创建新线程,而不是使用线程池线程。因此,其资源的占用和消耗要大于ThreadPool.QueueUserWorkItem
2、Task实现了IDispose接口,因此在用完task时要调用dispose,以释放资源。而不要使用GC回收
3、每个Task有唯一ID,可通过CurrentID获取,而CurrentID是一个可控类型的Int32
4、Task在生命周期中会有几个状态:
4.1 Createed //任务显示的创建完。可使用start方法,手动开始这个任务
4.2 WaitingForActivation //任务隐式创建。会自动开始。例如,通过continuewith开始的任务
4.3 WaitingToRun //已经进入调度,尚未开始
4.4 Runing
4.5 WaitingForChildrenToComplete
4.6 task最终的结果为:
4.6.1 RanToCompletion
4.6.2 Cancelled
4.6.3 Faulted
5、可以通过task的Status属性获取task的状态。同时Task提供了几个属性:
IsCanceled、IsCompleted、IsFaulted判断task的状态。但是有一个特殊情况:当Task的状态为RanToCompletion、Cancelled、Faulted中的任意状态时,调用Task的IsCompleted属性,其都将返回true。因此判断一个task是正常的完成的方式是:
if(task.Status==TaskStatus.RanToCompletion){ //.....}
任务工厂
有时候会遇到一种情况,用相同的配置创建多个Task。一种方法是挨个创建task;而另外一个方法就是使用任务工厂。(个人认为任务工厂使用的机会不会很多)
任务工厂同Task,也有两种方式,TaskFactory、TaskFactory<TResult>
。
可以通过TaskFactory的构造器创建TaskFactory实例,也可以使用Task类的TaskFactory属性进行创建,一般情况是使用后者。
TaskFactory具体的使用方法,可以参考MSDN的资料:TaskFactory
任务调度器
1、.NET中有多个类型的任务调度器,其中线程池使用的是“线程池线程任务调度器”,而GUI(WinForm、WPF、SilverLight)使用的则是“同步上下文任务调度器”,而通过不同的任务调度器所创建的task,其不允许相互操作。因此使用“线程池线程任务调度器”所创建的线程,不能操作界面(改变标题之类的操作),否则就会爆出InvalidOperationException
但是若希望线程池中的操作可以操作GUI上的元素,则其需通过TaskScheduler.FromCurrentSynchronizationContext()方法获得“同步上下文任务调度器”,在创建task时,将其作为参数传入。书中的例子很好,如下:
internal sealed class MyFrom:Form{ //Form的构造器,初始化标题等内容 public MyFrom(){ this.text="同步上下文任务调度器demo"; visible=true; width=400; height=400; } //通过TaskScheduler获得当前Form的“同步上下文任务调度器” private readonly TaskScheduler m_syncContextTaskScheduler= TaskScheduler.FromCurrentSynchronizationContext(); private CancellationTokeSource cts; //重写鼠标的点击方法 protected override void OnMouseClick(MouseEventArgs e){ if(cts!=null){ cts.cancel(); cts=null; }else{ text="操作开始"; cts = new CancellationTokeSource(); //下面的这个task使用线程池的任务调度器,也就是默认的任务调度器 var task = new task(()=>Sum(cts.Token,2000),cts.Token); task.ContinueWith( t=>Text="结果为"+t.Result, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, syncContextTaskScheduler ); task.ContinueWith( t=>Text="操作被取消", CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled, syncContextTaskScheduler ); task.ContinueWith( t=>Text="操作失败", CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, syncContextTaskScheduler ); }//else结束 base.OnMouseClick(e); }}
代码简单易懂,有一点很有意思。就是传入的参数中有:CancellationToken.None
,这个很有意思。其本质的想法就是,我创建的这个操作,不想被外界的cancel方法所取消,但是方法中还必须有一个这个参数,因此就使用CancellationToken.None
。这个CancellationToken.None
会返回一个CancellationToken,因其与任何的CancellationTokenSource没有任何关系,因此操作也就不能被取消了
26.4读书思考
1、task还是挺耗费资源的,但是又比Thread、ThreadPool等好用,没办法只能使用它
2、在26.4遇到一个问题,join与wait方法区别,有待解答!
联系客服