Task类使用总结

简介:

由于Framework 4.0和Framework 4.5对Task类稍微有些不同,此处声明以下代码都是基于Framework 4.5


Task类和Task<TResult>类,后者是前者的泛型版本。TResult类型为Task所调用方法的返回值。

主要区别在于Task构造函数接受的参数是Action委托,而Task<TResult>接受的是Func<TResult>委托。


 
 
  1. Task(Action) 
  2. Task<TResult>(Func<TResult>)  

启动一个任务 


 
 
  1. static void Main(string[] args) 
  2.         { 
  3.             Task Task1 = new Task(() => Console.WriteLine("Task1")); 
  4.             Task1.Start(); 
  5.             Console.ReadLine(); 
  6.         } 

通过实例化一个Task对象,然后Start,这种方式中规中矩,但是实践中,通常采用更方便快捷的方式

Task.Run(() => Console.WriteLine("Foo"));

这种方式直接运行了Task,不像上面的方法还需要调用Start();

Task.Run方法是Task类中的静态方法,接受的参数是委托。返回值是为该Task对象。

Task.Run(Action)

Task.Run<TResult>(Func<Task<TResult>>)

Task构造方法还有一个重载函数如下:

Task 构造函数 (Action, TaskCreationOptions),对应的Task泛型版本也有类似构造函数。TaskCreationOptions参数指示Task创建和执行的可选行为。常用的参数LongRunning。

默认情况下,Task任务是由线程池线程异步执行的。如果是运行时间很长的操作,使用LongRunning 参数暗示任务调度器,将这个任务放在非线程池上运行。通常不需要用这个参数,除非通过性能测试觉得使用该参数能有更好的性能,才使用。

任务等待

默认情况下,Task任务是由线程池线程异步执行。要知道Task任务的是否完成,可以通过task.IsCompleted属性获得,也可以使用task.Wait来等待Task完成。Wait会阻塞当前线程。 


 
 
  1. static void Main(string[] args) 
  2.         { 
  3.             Task Task1=Task.Run(() => { Thread.Sleep(5000); 
  4.             Console.WriteLine("Foo"); 
  5.                 Thread.Sleep(5000); 
  6.             }); 
  7.             Console.WriteLine(Task1.IsCompleted); 
  8.             Task1.Wait();//阻塞当前线程 
  9.             Console.WriteLine(Task1.IsCompleted); 
  10.         } 

Wait方法有个重构方法,签名如下:public bool Wait(int millisecondsTimeout),接受一个时间。如果在设定时间内完成就返回true,否则返回false。如下的代码:


 
 
  1. static void Main(string[] args) 
  2.         { 
  3.             Task Task1=Task.Run(() => { Thread.Sleep(5000); 
  4.             Console.WriteLine("Foo"); 
  5.                 Thread.Sleep(5000); 
  6.             }); 
  7.  
  8.             Console.WriteLine("Task1.IsCompleted:{0}",Task1.IsCompleted); 
  9.             bool b=Task1.Wait(2000); 
  10.             Console.WriteLine("Task1.IsCompleted:{0}", Task1.IsCompleted); 
  11.             Console.WriteLine(b); 
  12.             Thread.Sleep(9000); 
  13.             Console.WriteLine("Task1.IsCompleted:{0}", Task1.IsCompleted); 
  14.        } 

运行结果为:

83987B3EBCAD4B198297D125DF6B849A

获得返回值 

要获得返回值,就要用到Task的泛型版本了。 


 
 
  1. static void Main(string[] args) 
  2.         { 
  3.             Task<int> Task1 = Task.Run<int>(() => { Thread.Sleep(5000); return Enumerable.Range(1, 100).Sum(); }); 
  4.             Console.WriteLine("Task1.IsCompleted:{0}",Task1.IsCompleted); 
  5.             Console.WriteLine("Task1.IsCompleted:{0}", Task1.Result);//如果方法未完成,则会等待直到计算完成,得到返回值才运行下去。 
  6.             Console.WriteLine("Task1.IsCompleted:{0}", Task1.IsCompleted); 
  7.         } 

结果如下:

EEA94E6174324CFAB04312D10DB3AABD

异常抛出

和线程不同,Task中抛出的异常可以捕获,但是也不是直接捕获,而是由调用Wait()方法或者访问Result属性的时候,由他们获得异常,将这个异常包装成AggregateException类型,再重新抛出捕获。 


 
 
  1. static void Main(string[] args) 
  2.         { 
  3.             try 
  4.             { 
  5.                 Task<int> Task1 = Task.Run<int>(() => { throw new Exception("xxxxxx"); return 1; }); 
  6.                 Task1.Wait(); 
  7.             } 
  8.             catch (Exception ex)//error的类型为System.AggregateException 
  9.             { 
  10.                 Console.WriteLine(ex.StackTrace); 
  11.                 Console.WriteLine("-----------------"); 
  12.                 Console.WriteLine(ex.InnerException.StackTrace); 
  13.             } 
  14.         } 

如上代码,运行结果如下:

D4DA781C92F149E897BE8C325F8A90C0

可以看到异常真正发生的地方。

对于某些匿名的Task(通过 Task.Run方法生成的,不调用wait,也不关心是否运行完成),某些情况下,记录它们的异常错误也是有必要的。这些异常称作未观察到的异常(unobserved exceptions)。可以通过订阅一个全局的静态事件TaskScheduler.UnobservedTaskException来处理这些异常。只要当一个Task有异常,并且在被垃圾回收的时候,才会触发这一个事件。如果Task还处于被引用状态,或者只要GC不回收这个Task,这个UnobservedTaskException事件就不会被触发

例子: 


 
 
  1. static void Main(string[] args) 
  2.         { 
  3.             TaskScheduler.UnobservedTaskException += UnobservedTaskException; 
  4.             Task.Run<int>(() => { throw new Exception("xxxxxx"); return 1; }); 
  5.             Thread.Sleep(1000); 
  6.        } 
  7.  
  8.         static void UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) 
  9.         { 
  10.             Console.WriteLine(e.Exception.Message); 
  11.             Console.WriteLine(e.Exception.InnerException.Message); 
  12.         } 

这样的代码直到程序运行完成也为未能触发UnobservedTaskException,因为GC没有开始做垃圾回收。

在代码中加入 GC.Collect(); 


 
 
  1. static void Main(string[] args) 
  2.         { 
  3.             TaskScheduler.UnobservedTaskException += UnobservedTaskException; 
  4.             Task.Run<int>(() => { throw new Exception("xxxxxx"); return 1; }); 
  5.             Thread.Sleep(1000); 
  6.             GC.Collect(); 
  7.             GC.WaitForPendingFinalizers(); 
  8.         } 

运行后得到如下:

6E202F46BB6B4FE384CDD49A564D2A7E

延续任务

延续任务就是说当一个Task完成后,继续运行下一个任务。通常有2种方法实现。

一种是使用GetAwaiter方法。GetAwaiter方法返回一个TaskAwaiter结构,该结构有一个OnCompleted事件,只需对OnCompleted事件赋值,即可在完成后调用该事件。 


 
 
  1. static void Main(string[] args) 
  2.         { 
  3.             Task<int> Task1 = Task.Run<int>(() => { return Enumerable.Range(1, 100).Sum(); }); 
  4.             var awaiter = Task1.GetAwaiter(); 
  5.             awaiter.OnCompleted(() => 
  6.             { 
  7.                 Console.WriteLine("Task1 finished"); 
  8.                 int result = awaiter.GetResult(); 
  9.                 Console.WriteLine(result); // Writes result 
  10.             }); 
  11.             Thread.Sleep(1000); 
  12.         } 

运行结果如下:

0A31FA65B04B477A92175A8184AE2857

此处调用GetResult()的好处在于,一旦先前的Task有异常,就会抛出该异常。而且该异常和之前演示的异常不同,它不需要经过AggregateException再包装了。

另一种延续任务的方法是调用ContinueWith方法。ContinueWith返回的任然是一个Task类型。ContinueWith方法有很多重载,算上泛型版本,差不多40个左右的。其中最常用的,就是接受一个Action或者Func委托,而且,这些委托的第一个传入参数都是Task类型,即可以访问先前的Task对象。示例: 


 
 
  1. static void Main(string[] args) 
  2.         { 
  3.             Task<int> Task1 = Task.Run<int>(() => {return Enumerable.Range(1, 100).Sum(); }); 
  4.             Task1.ContinueWith(antecedent => { 
  5. Console.WriteLine(antecedent.Result); 
  6. Console.WriteLine("Runing Continue Task"); 
  7. }); 
  8.             Thread.Sleep(1000); 
  9.         } 

使用这种ContinueWith方法和GetAwaiter都能实现相同的效果,有点小区别就是ContinueWith如果获取Result的时候有异常,抛出的异常类型是经过AggregateException包裹的,而GetAwaiter()后的OnCompleted所调用的方法中,如果出错,直接抛出异常。 

生成Task的另一种方法,TaskCompletionSource 

使用TaskCompletionSource很简单,只需要实例化它即可。TaskCompletionSource有一个Task属性,你可以对该属性暴露的task做操作,比如让它wait或者ContinueWith等操作。当然,这个task由TaskCompletionSource完全控制。TaskCompletionSource<TResult>类中有一些成员方法如下:


 
 
  1. public class TaskCompletionSource<TResult> 
  2. public void SetResult (TResult result); 
  3. public void SetException (Exception exception); 
  4. public void SetCanceled(); 
  5. public bool TrySetResult (TResult result); 
  6. public bool TrySetException (Exception exception); 
  7. public bool TrySetCanceled(); 
  8. ... 

调用以上方法意味着对Task做状态的改变,将状态设成completed,faulted或者 canceled。这些方法只能调用一次,不然会有异常。Try的方法可以调多次,只不过返回false而已。

通过一些技巧性的编码,将线程和Task协调起来,通过Task获得线程运行的结果。

示例代码: 


 
 
  1. static void Main(string[] args) 
  2.         { 
  3.             var tcs = new TaskCompletionSource<int>(); 
  4.             new Thread(() => { 
  5.                 Thread.Sleep(5000); 
  6.                 int i = Enumerable.Range(1, 100).Sum(); 
  7.                 tcs.SetResult(i); }).Start();//线程把运行计算结果,设为tcs的Result。 
  8.             Task<int> task = tcs.Task; 
  9.             Console.WriteLine(task.Result); //此处会阻塞,直到匿名线程调用tcs.SetResult(i)完毕 
  10.         } 

说明一下以上代码:

tcs是TaskCompletionSource<int>的一个实例,即这个Task返回的肯定是一个int类型。

获得tcs的Task属性,读取并打印该属性的值。那么 Console.WriteLine(task.Result);其实是会阻塞的,直到task的result被赋值之后,才会取消阻塞。而对task.result的赋值正在一个匿名线程中做的。也就是说,一直等到匿名线程运行结束,把运行结果赋值给tcs后,task.Result的值才会被获得。这正是变相的实现了线程同步的功能,并且可以获得线程的运行值。而此时的线程并不是运行在线程池上的

我们可以定义一个泛型方法,来实现一个Task对象,并且运行Task的线程不是线程池线程:


 
 
  1. Task<TResult> Run<TResult>(Func<TResult> function) 
  2.         { 
  3.             var tcs = new TaskCompletionSource<TResult>(); 
  4.             Thread t = new Thread(() => 
  5.              { 
  6.                  try { tcs.SetResult(function()); } 
  7.                  catch (Exception ex) { tcs.SetException(ex); } 
  8.              }); 
  9.             t.IsBackground = true
  10.             t.Start();//启动线程 
  11.             return tcs.Task; 
  12.         } 

比如什么一个泛型方法,接受的参数是Func委托,返回的是Task类型。

该方法中启动一个线程t,把t设为后台线程,该线程运行的内容就是传入的Func委托,并将Func委托的运行后的返回值通过tcs.SetResult赋给某个task。同时,如果有异常的话,就把异常赋给,某个task,然后将这个task返回。这样,直到线程运行完毕,才能得到task.Result的值。调用的时候: 


 
 
  1. Task<int> task = Run(() => { Thread.Sleep(5000); return Enumerable.Range(1, 100).Sum(); }); 
  2. Console.Write(task.Result);//这句会阻塞当前线程,直到task的result值被赋值才行。 

TaskCompletionSource的另一个强大用处,是可以创建Task,而不绑定任何线程,比如,我们可以通过TaskCompletionSource实现对某一个方法的延迟调用。

代码示例: 


 
 
  1. static Task<int> delayFunc() 
  2.         { 
  3.             var tcs = new TaskCompletionSource<int>(); 
  4.             var timer = new System.Timers.Timer(5000) { AutoReset = false }; 
  5.             timer.Elapsed += (sender, e) => { 
  6.                 timer.Dispose(); 
  7.                 int i = Enumerable.Range(1, 100).Sum(); 
  8.                 tcs.SetResult(i); 
  9.             }; 
  10.  
  11.             timer.Start(); 
  12.             return tcs.Task; 
  13.         } 

说明:

delayFunc()方法使用了一个定时器,5秒后,定时器事件触发,将i的值赋给某个task的result。返回的是tcs.Task属性,调用方式: 


 
 
  1. var task = delayFunc(); 
  2. Console.Write(task.Result); 

task变量得到赋值后,要读取Result值,必须等到tcs.SetResult(i);运行完成才行。这就相当于实现了延迟某个方法。

当然Task自身提供了Delay方法,使用方法如下: 


 
 
  1. Task.Delay (5000).GetAwaiter().OnCompleted (() => Console.WriteLine (42)); 
  2. 或者: 
  3. Task.Delay (5000).ContinueWith (ant => Console.WriteLine (42)); 

Delay方法是相当于异步的Thread.Sleep();

---------------------------------

参考资料:《C# 5.0 IN A NUTSHELL》

MSDN官方资料
















本文转自cnn23711151CTO博客,原文链接: http://blog.51cto.com/cnn237111/1102476,如需转载请自行联系原作者


相关文章
|
11月前
|
网络协议 算法 程序员
第十问:TCP协议是怎么做到可靠性的?它的可靠指的是到哪一层的可靠?
TCP(传输控制协议)是一种面向连接的传输层协议,其核心特性是可靠性。TCP通过数据分片与排序、确认机制(ACK)、超时重传、流量控制、拥塞控制、校验和等机制,确保数据从发送方到接收方的完整性和有序性。这些机制共同作用,使TCP能够在复杂网络环境中实现稳定的数据传输。TCP的可靠性主要指的是从传输层到传输层的可靠性,传输层之上的可靠性则由应用程序负责。
|
9月前
|
人工智能 Cloud Native 安全
DeepSeek + Higress AI 网关/Spring AI Alibaba 案例征集
诚挚地感谢每一位持续关注并使用 Higress 和 Spring AI Alibaba 的朋友,DeepSeek + Higress AI 网关/Spring AI Alibaba 案例征集中。
754 107
|
9月前
|
SQL 关系型数据库 MySQL
基于SQL Server / MySQL进行百万条数据过滤优化方案
对百万级别数据进行高效过滤查询,需要综合使用索引、查询优化、表分区、统计信息和视图等技术手段。通过合理的数据库设计和查询优化,可以显著提升查询性能,确保系统的高效稳定运行。
393 9
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
486 10
|
监控 安全 网络协议
设备组态网络应用与通信系统
设备组态网络应用与通信系统
|
Web App开发 域名解析 监控
前端可观测性的宣讲-1022
前端可观测性的宣讲-1022
636 0
前端可观测性的宣讲-1022
|
消息中间件 小程序 Java
暹罗点餐开源啦,一款java多门店点餐系统-连锁门店如蜜雪冰城瑞幸咖啡
暹罗点餐是一款Java餐饮点餐系统,适用于多门店的连锁品牌,对标蜜雪冰城、瑞幸咖啡。系统包含用户端、商家端、配送端以及总管理后台; * 前端使用uni-app开发,可打包部署到微信小程序、APP、H5 * Web端使用vue + Element开发 * 服务端使用java语言开发,技术栈:Spring Boot + Redis + RocketMQ + WebSocket + ElasticSearch + ELK + SpringBoot Admin
976 1
暹罗点餐开源啦,一款java多门店点餐系统-连锁门店如蜜雪冰城瑞幸咖啡
|
消息中间件 缓存 运维
阿里云IoT物联网实例视频讲解、场景案例汇总
阿里云IoT物联网实例视频讲解、场景案例汇总
2152 3
|
缓存 网络协议 Linux
《Linux高性能服务器编程》——3.8 带外数据
本节书摘来自华章计算机《Linux高性能服务器编程》一书中的第3章,第3.8节,作者 游双,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
2094 0
|
存储 缓存 人工智能
揭秘 LlamaIndex|如何持久化存储 LlamaIndex 向量索引?
LlamaIndex 作为一个专为构建 LLM 应用设计的新工具,它可以为用户抽象出上述框架中的内容。
1277 0