C#多线程的基础整理(中)——Task和async await
来源:互联网 发布:mac的话筒在哪 编辑:程序博客网 时间:2024/05/22 14:21
前言
关于C#多线程的使用,想必网上已经有很多前辈进行过详细的总结。笔者在此再次进行整理,主要目的在于对自己学到的东西做一个总结和记录。本人接触C#的时间尚短,想必在很多地方还有缺陷,还请可能存在的读者们予以谅解并加以指教;如果本文真的能帮助到一些读者的话,那笔者实在是不胜荣幸。
更好的多线程使用方法——Task
上一篇文章中笔者对Thread
和ThreadPool
的基础使用方法进行了整理,并提到了Task
。那么这一篇文章,就来开始介绍Task
。Task
位于System.Threading.Tasks
命名空间中,其简单的使用方法和Thread
有些类似,都可以通过函数(和参数)来创建并开始运行:
static void Func1(){ int r = 0; //一个耗时的操作 for (int i = 0; i != int.MaxValue; ++i) { ++r; } Console.Out.WriteLine(r);}static void Func2(object obj){ Thread.Sleep(1000); //模拟一个耗时的操作 Console.Out.WriteLine("Func2 is running with {0}", obj as string);}static void Main(string[] args){ //如果不需要,可以不保留Task对象, //直接进行执行 new Task(Func1).Start(); //使用了Run方法的版本 //Task.Run(() => Func1()); Task.Run(() => Func2("Task")); //使用了Start方法的版本 //便于理解如何为Task传递参数 //new Task(Func2, "Task").Start(); Console.In.Read();}
以上的代码通过Task
创建了两个任务,分别运行Func1
和Func2
。Main
中的第一行代码创建了Task
对象并立刻调用了Start
,实际使用中也可以先创建完Task
对象后再调用Start
。但是按照《CLR via C#》一书中的说法,创建Task
对象并立即调用Start
是常见的编程模式,因此Task
类中也提供了静态方法Run
用于创建并调用任务。Main
中的第二行代码便是使用了Run
方法,其参数是一个lambda表达式,并在lambda表达式生成的匿名函数中调用了Func2
。通过Task
创建的线程和ThreadPool
中的一样,是后台线程。
Task
支持的函数和Thread
类相似,它支持没有参数或者有一个object
参数的函数,同时返回值类型为void
。但是上一篇文章说过了,Task
解决了Thread
和ThreadPool
无法获得函数返回值的问题,因此Task
并不是只能使用返回值类型为void
的函数。为了使用带有返回值的函数,就需要引入Task<TResult>
。
用Task获取函数返回值
Task<TResult>
是派生自Task
的一个泛型版本,其中TResult
代表了函数的返回值,可以是任意非void
类型。
static int Func1(){ int r = 0; //一个耗时的操作 for (int i = 0; i != int.MaxValue; ++i) { ++r; } return r;}static void Main(string[] args){ Task<int> t = new Task<int>(Func1); t.Start(); //获取t的运行结果 int result = t.Result; Console.Out.WriteLine(result); Console.In.Read();}
上述代码中先建立了一个Task<int>
,并在之后调用了Start
使其开始执行。随后我们声明了一个变量,并通过t.Result
获取函数的返回值。如果调用t.Result
时Task
还未执行完,那么就会和Thread
的Join
方法一样,造成当前线程阻塞。在上述例子中,这样的使用方法可能显得和单线程无异。我们可以通过增加一些小的修改达成一些有趣的效果:
static int Func1(){ int r = 0; //一个耗时的操作 for (int i = 0; i != int.MaxValue; ++i) { ++r; } return r;}static void Main(string[] args){ Task<int> t = new Task<int>(Func1); t.Start(); Console.Write("waiting"); //这里的循环条件并不严谨 while(t.Status != TaskStatus.RanToCompletion) { //每500毫秒输出一个. Console.Write("."); Thread.Sleep(500); } Console.WriteLine(); //获取t的运行结果 int result = t.Result; Console.Out.WriteLine(result); Console.In.Read();}
以上代码会在等待t
执行时,输出waiting
并随着等待时间向waiting
后面增加.
,模仿了一个类似进度条的功能。当t
完成之后,便和上一个代码相同,获取t
的运行结果并输出。也许这一点小改动没什么实际的意义,但是至少现在能看到一些多线程的效果了。
但是请注意,上述代码从理论上讲并不严谨——只以任务状态不为RanToCompletion
作为循环中断的条件是不正确的。t
结束运行可能有三种情况:完成、被取消和出现错误,分别对应TaskStatus
的RanToCompletion
、Canceled
、Faulted
。其中出现错误可能是因为出现异常而导致线程被中断,而被取消则涉及到了CancellationToken
相关的操作。限于篇幅,Task
的异常处理和取消操作在这里先不涉及,笔者可能会在以后的其它博文中再进行整理。在此奉上MSDN中的相关信息,供有兴趣的读者查询:取消,异常处理。
当任务完成时自动启动新任务
经过修改的代码,虽然可以展示出一定的多线程特性,但是其本质上还是使线程无法接受用户操作了。一款好的软件应当尽量不使线程阻塞,不然可能会使用户只能等待程序结束当前工作而无法继续操作。因此除非开发人员真的认为有必要,不然应当尽量避免在线程中调用Wait
和Result
,并采用当任务结束时自动执行后续操作的方式。 Task
类提供了ContinueWith
方法,可以在Task完成后自动启动指定的新任务。基本的使用方法如下所示:
static int Func1(object obj){ //改为了可设置参数的版本 int r = 0; int l = (int)obj; for (int i = 0; i != l; ++i) { ++r; } return r;}static void Func2(Task<int> t){ //参数为前置运行的Task对象 Console.Out.WriteLine(t.Result);}static void Main(string[] args){ Task<int> t = new Task<int>(Func1,2000000000); //在t结束后执行Func2 t.ContinueWith(Func2); t.Start(); Console.In.Read();}
通过调用ContinueWith
方法,我们指定t
在完成后执行Func2
来输出结果。在实际使用中,当然也可以执行用返回值进行进一步计算等操作,需要灵活运用。
以上就是Task
的基本使用方法了。其实Task
还有很多其它特性没有说到,笔者打算在后续的博文中继续进行整理,有兴趣的读者也可以参考MSDN。
异步编程——async/await
介绍过Task
的基础操作之后,笔者接下来打算介绍一下C#中的异步编程关键字——async
和await
。这是C#5.0中引入的特性,其在内部使用了Task
和状态机,能够令使用者以线性的思维进行多线程程序的开发。加入了关键字async
的函数即被声明为异步函数,而被声明为异步函数后才能在函数中使用await
关键字。异步函数的返回值只能是Task
,Task<TResult>
,或者void
。当一个异步函数遇到await
关键字后,这个函数会立刻返回;当await
的操作结束后,会有另一个线程池线程开始继续执行await
后的语句。将一直使用的示例程序改写为使用async
/await
后的代码如下:
static async Task<int> Func1(int l){ //改写为异步函数 return await Task.Run(() => { int r = 0; for (int i = 0; i != l; ++i) { ++r; } return r; }); //另一种方式 /* return await new Task<int>(() => { int r = 0; for (int i = 0; i != l; ++i) { ++r; } return r; }); */}static async void Test(){ //遇到了await关键字,该函数立刻返回 //此时await关键字会在需要等待的Task对象上调用ContinueWith //当这个Task结束后有另一个线程从await的地方继续执行其余代码 int result = await Func1(2000000000); //Func1的操作结束后,另一个线程池线程重新进入Test函数 //因await中断的地方,将结果进行输出 Console.Out.WriteLine(result);}static void Main(string[] args){ //Main不能带有关键字async Test(); Console.In.Read();}
忽略掉有些冗长的注释,可以发现现在这个程序的逻辑很清楚:Main
调用Test
函数,Test
中计算Func1(2000000000)
并将其输出。整个流程和一个单线程程序相近,但是这个程序并不会造成阻塞:当Test
中遇到await
关键字时,Test
函数就返回了;等到Func1
的计算结束后,另一个线程进入并继续执行await
之后的代码,即输出result
。
使用await
关键字在效果上和使用Task<TResult>
的Wait
或Result
比较相近,await
返回的值正是要等待的的异步操作的返回结果,在示例代码中就是Func1
的返回值,但是如果真的使用Wait
或Result
则会导致线程阻塞。如果使用Task
并调用ContinueWith
来实现相同的效果,那么又会导致代码被分为Task
内的部分和ContinueWith
的部分,相比之下没有使用await
逻辑清晰。
还有一些关于异步函数返回值的问题需要解释一下。上文中说过加入了async
关键字的异步函数的返回值只能为Task
,Task<TResult>
,或者void
,但这指的是函数声明的返回值,而实际在函数内的返回值(即return
的对象)应该是TResult
类型(当声明的返回值为Task<TResult>
时)或者为空(当声明的返回值为Task
或者void
时)。需要这样做的理由与编译器对async
和await
的实现有关,笔者没有自信能解释透彻,故在此不做深究,感兴趣的读者请自行查阅资料。 当实际的返回值为空时,函数声明时的返回值有Task
和void
两种,其中以Task
作为返回值的函数允许进行await
操作,而以void
为返回值的函数则不允许await
。
在实际使用中,可能还会需要用到await
的异常处理和取消操作,相关的内容笔者会在之后的博文中继续整理。
后记
一开始产生写博客的想法,其实是在自己尝试实现一个C#的网络通信框架的时候,因为了解到了C#中还可以通过async
和await
来进行异步编程,才打算写一篇博客记录相关的内容,结果就变成了将多线程相关的基础操作都整理了了一下了。在这一篇博文中,我才终于写到了最开始打算写的内容,然而使用async
和await
的异步编程方式真的很简洁,如果只是初步的使用的话并不十分复杂,所以我真正想记录的内容在最后并没有占很多篇幅。不过这也并不算是本末倒置,在写博客的过程中我也的确发现了很多没有注意的问题,总归还是有很多收获。写博客中发现的问题,还有很多多线程相关的使用细节,我打算放在之后的博客中再进行整理,有关基础使用的地方就先在此告一段落了。想必在我写的博客中还存在很多问题,还请各位读者指出;如果我写的这些东西真的帮到了一些读者,那么我在此深表荣幸。
- C#多线程的基础整理(中)——Task和async await
- C#中 Thread,Task,Async/Await,IAsyncResult 的那些事儿!
- C#中 Thread,Task,Async/Await,IAsyncResult 的那些事儿!
- c# 5.0——async 和 await
- Async和await以及Task的爱恨情仇
- C#的async和await
- C#的async和await
- C#的async和await
- C#中Task任务和Async、Await异步非阻塞方式
- C# async和await
- C#里的async和await
- 说说C#的async和await
- 说说C#的async和await
- 说说C#的async和await
- 说说C#的async和await
- C# 5.0 新特性——Async和Await
- 初识C#异步编程Task,await,async
- C# -- Async 和 Await 解惑
- 代码重构(一)——总结代码的坏味道
- from scipy.linalg import _fblas ImportError: DLL load failed: 找不到指定的模块
- leetcode: 75. Sort Colors
- go 字符串的拼接
- 安装phpMyAdmin数据库图形管理
- C#多线程的基础整理(中)——Task和async await
- Codeforces 892/E Envy 最小生成树的query
- iOS开发-RuntimeDebug运行时源码调试(macOS APP)
- C#-动态编程
- .NET Entity Framework(EF)使用SqlQuery直接操作SQL查询语句或者执行过程
- C#获取本机MAC地址和IP
- (基于UDP协议/tcp协议)socket客户端,服务端
- leetcode: 76. Minimum Window Substring
- Netty学习(二)—拆包粘包问题