C# Task
![huuhghhgyg avatar](https://www.zhhuu.top/img/favicon.jpg)
- huuhghhgyg
- 1 min read
![C# Task](/img/random/material-6.webp)
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 标志着这些代码是异步的,编译器必须重新排列它。