C#多线程的基础整理(中)——Task和async await

来源:互联网 发布:mac的话筒在哪 编辑:程序博客网 时间:2024/05/22 14:21

前言

关于C#多线程的使用,想必网上已经有很多前辈进行过详细的总结。笔者在此再次进行整理,主要目的在于对自己学到的东西做一个总结和记录。本人接触C#的时间尚短,想必在很多地方还有缺陷,还请可能存在的读者们予以谅解并加以指教;如果本文真的能帮助到一些读者的话,那笔者实在是不胜荣幸。

更好的多线程使用方法——Task

上一篇文章中笔者对ThreadThreadPool的基础使用方法进行了整理,并提到了Task。那么这一篇文章,就来开始介绍TaskTask位于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创建了两个任务,分别运行Func1Func2Main中的第一行代码创建了Task对象并立刻调用了Start,实际使用中也可以先创建完Task对象后再调用Start。但是按照《CLR via C#》一书中的说法,创建Task对象并立即调用Start是常见的编程模式,因此Task类中也提供了静态方法Run用于创建并调用任务。Main中的第二行代码便是使用了Run方法,其参数是一个lambda表达式,并在lambda表达式生成的匿名函数中调用了Func2。通过Task创建的线程和ThreadPool中的一样,是后台线程。

Task支持的函数和Thread类相似,它支持没有参数或者有一个object参数的函数,同时返回值类型为void。但是上一篇文章说过了,Task解决了ThreadThreadPool无法获得函数返回值的问题,因此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.ResultTask还未执行完,那么就会和ThreadJoin方法一样,造成当前线程阻塞。在上述例子中,这样的使用方法可能显得和单线程无异。我们可以通过增加一些小的修改达成一些有趣的效果:

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结束运行可能有三种情况:完成、被取消和出现错误,分别对应TaskStatusRanToCompletionCanceledFaulted。其中出现错误可能是因为出现异常而导致线程被中断,而被取消则涉及到了CancellationToken相关的操作。限于篇幅,Task的异常处理和取消操作在这里先不涉及,笔者可能会在以后的其它博文中再进行整理。在此奉上MSDN中的相关信息,供有兴趣的读者查询:取消,异常处理。

当任务完成时自动启动新任务

经过修改的代码,虽然可以展示出一定的多线程特性,但是其本质上还是使线程无法接受用户操作了。一款好的软件应当尽量不使线程阻塞,不然可能会使用户只能等待程序结束当前工作而无法继续操作。因此除非开发人员真的认为有必要,不然应当尽量避免在线程中调用WaitResult,并采用当任务结束时自动执行后续操作的方式。
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#中的异步编程关键字——asyncawait。这是C#5.0中引入的特性,其在内部使用了Task和状态机,能够令使用者以线性的思维进行多线程程序的开发。加入了关键字async的函数即被声明为异步函数,而被声明为异步函数后才能在函数中使用await关键字。异步函数的返回值只能是TaskTask<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>WaitResult比较相近,await返回的值正是要等待的的异步操作的返回结果,在示例代码中就是Func1的返回值,但是如果真的使用WaitResult则会导致线程阻塞。如果使用Task并调用ContinueWith来实现相同的效果,那么又会导致代码被分为Task内的部分和ContinueWith的部分,相比之下没有使用await逻辑清晰。

还有一些关于异步函数返回值的问题需要解释一下。上文中说过加入了async关键字的异步函数的返回值只能为TaskTask<TResult>,或者void,但这指的是函数声明的返回值,而实际在函数内的返回值(即return的对象)应该是TResult类型(当声明的返回值为Task<TResult>时)或者为空(当声明的返回值为Task或者void时)。需要这样做的理由与编译器对asyncawait的实现有关,笔者没有自信能解释透彻,故在此不做深究,感兴趣的读者请自行查阅资料。 当实际的返回值为空时,函数声明时的返回值有Taskvoid两种,其中以Task作为返回值的函数允许进行await操作,而以void为返回值的函数则不允许await

在实际使用中,可能还会需要用到await的异常处理和取消操作,相关的内容笔者会在之后的博文中继续整理。

后记

一开始产生写博客的想法,其实是在自己尝试实现一个C#的网络通信框架的时候,因为了解到了C#中还可以通过asyncawait来进行异步编程,才打算写一篇博客记录相关的内容,结果就变成了将多线程相关的基础操作都整理了了一下了。在这一篇博文中,我才终于写到了最开始打算写的内容,然而使用asyncawait的异步编程方式真的很简洁,如果只是初步的使用的话并不十分复杂,所以我真正想记录的内容在最后并没有占很多篇幅。不过这也并不算是本末倒置,在写博客的过程中我也的确发现了很多没有注意的问题,总归还是有很多收获。写博客中发现的问题,还有很多多线程相关的使用细节,我打算放在之后的博客中再进行整理,有关基础使用的地方就先在此告一段落了。想必在我写的博客中还存在很多问题,还请各位读者指出;如果我写的这些东西真的帮到了一些读者,那么我在此深表荣幸。

原创粉丝点击