C# Task
 
        - huuhghhgyg
- 1 min read
 
Task
线程的问题:线程(Thread)是用来创建并发的一种低级别工具,尤其在于以下方面有一些限制:
- 虽然开始线程的时候可以方便地传入数据,但是当Join(等待)的时候,很难从线程获得返回值。- 可能需要设置一些共享字段
- 如果抛出异常,捕获和传播该异常都很麻烦
 
- 无法告诉线程在结束时开始做另外的工作,必须进行Join操作,在进程中阻塞当前的进程,等待线程结束。
- 对手动同步的更大依赖以及随之而来的问题
Task类可以很好地解决上述问题。Task是一个相对高级的抽象,它代表了一个并发操作(concurrent),该操作可能由Thread支持,或不由Thread支持。并且,Task是可组合的。
- Tasks可以使用线程池来减少启动延迟
- 使用TaskCompletionSource,Tasks可以利用回调的方式在等待I/O绑定操作是完全避免线程。
开始一个Task
开始一个Task最简单的方法就是使用Task.Run这个静态方法。(.NET4.5开始,.NET4.0的时候是Task.Factory.StartNew这个静态方法) 使用方法:传入一个Action委托即可。
Task默认使用线程池,也就是后台线程。当主线程结束时,创建的所有任务都会结束。
1static void Main(string[] args)
2{
3    Task.Run(() => Console.WriteLine("Foo"));
4    Console.ReadLine(); // 可以达到阻塞线程的效果。
5    // 如果程序运行完了,由于Task是后台线程,Task也会被关闭。
6}
Task.Run返回一个Task对象,可以使用它来监视其过程。Task.Run之后没有调用Start,因为该方法创建的是“热”任务(hot task);可以通过Task的构造函数创建“冷”任务(cold task),但很少这样做。
Wait
调用Task的Wait方法会进行阻塞直到操作完成(相当于调用Thread上的Join方法)
 1static void Main(string[] args)
 2{
 3    Task task = Task.Run(() =>
 4    {
 5        Thread.Sleep(3000);
 6        Console.WriteLine("Foo");
 7    }); //创建一个“热任务”
 8
 9    Console.WriteLine(task.IsCompleted); //False
10
11    task.Wait(); //阻塞至task完成操作
12
13    Console.WriteLine(task.IsCompleted); //True
14
15    // False
16    // Foo
17    // True
18}
Wait也可以指定一个超时时间和取消令牌来提前结束等待。
长时间运行的任务
- 默认情况下,CLR在线程池中运行Task,这非常适合短时间运行的Compute-Bound类工作
- 针对长时间运行的任务或者阻塞操作(例如前面的例子),可以不采用线程池
- 如果同时运行多个long-running tasks(尤其是其中有处于阻塞态的),那么性能将会受很大影响,这时有比TaskCreationOptions.LongRunning更好的办法:- 如果任务是IO-Bound,TaskCompletionSource和异步函数可以让你用回调代替线程实现并发。
- 如果任务是Compute-Bound,生产者/消费者队列允许你对任务的并发性进行限流,避免把其它线程和进程饿死。(并行编程)
 
- 如果任务是IO-Bound,
Task的返回值
Task有一个泛型子类叫做Task<TResult>,他允许发出一个返回值。
使用Func<TResult>委托或兼容的Lambda表达式来调用Task.Run就可以得到Task<TResult>,随后可以通过Result属性来获得返回的结果。
如果这个task还没有完成操作,访问Result属性会阻塞该线程直到该task完成操作。
 1static void Main(string[] args)
 2{
 3    Task<int> task = Task.Run(() =>
 4    {
 5        Console.WriteLIne("Foo");
 6        return 3;
 7    }); //Lambda表达式与Func<TResult>兼容
 8
 9    int result = task.Result; //如果task没完成,会阻塞当前进程,直到返回结果
10    Console.WriteLine(result); //3
11}
Task<TResult>可以看作是一种所谓得“许诺”,在它里面包裹着一个Result,在稍后的时候就会变得可用。
Task的异常
不同于Thread,Task可以很方便地传播异常。如果task里面抛出了ige未处理的异常(故障),那么异常就会重新抛出给:
- 调用了wait()的地方、
- 访问了Task<TResult>的Result属性的地方
无需重新抛出异常,通过Task的IsFaulted和IsCancelled属性也可以检测出Task是否发生了故障:
- 如果两个属性都返回false,那么没有错误发生。
- 如果IsCanceled位true,那就说明一个OperationCanceledException为该Task抛出了异常
- 如果IsFaulted为true,那就说明另一个类型的异常被抛出,而Exception属性也将指明错误
“自治”的Task
“自治”的Task制“设置完就不管了”的Task。指不通过调用Wait()方法、Result属性或continuation进行会合的任务。
针对自治的Task,需要像Thread一样,显式地处理异常,避免发生“悄无声息”的故障。 自治Task上未处理的异常称为未观察到的异常。
未观察到的异常
可以通过全局的TaskScheduler.UnobservedTaskException来订阅未观察到的异常。
使用超时进行等待的Task,如果在超时后发生故障,那么它将会产生一个“未观察到的异常”
在Task发生故障后,如果访问Task的Exception属性,那么该异常就被认为是“已观察到的异常”
Continuation
当Task结束的时候,继续做其它事情。Continuation通常是通过回调的方式实现的,当操作一结束,就开始执行。
在Task上调用GetAwaiter会返回一个awaiter对象。它的OnCompleted方法会告诉task:“当你结束/发生故障时要执行委托”
可以将Continuation附加到已经结束的task上面,此时Continuation将会被安排立即执行。
Awaiter
awaiter是可以暴露下列两个方法和一个属性的对象:
- OnCompleted
- GetResult
- IsCompleted(bool属性)
其中,OnCompleted是INotifyCompletion的一部分
如果发生故障,调用awaiter.GetResult()的时候,异常会重新抛出。
无需调用GetResult,task的Result属性也可以直接访问。
非泛型Task
非泛型的task,GetResult()方法有一个void返回值,只用来重新抛出异常。
未完…
async 标志着这些代码是异步的,编译器必须重新排列它。