异步编程的基础

async和await关键字只是编译器功能。编译器会用Task类创建代码。如果不使用这两个关键字,也可以用C# 4.0和Task类的方法来实现同样的功能,只是没有那么方便。

本节介绍了编译器用async和await关键字能做什么,如何采用简单的方式创建异步方法,如何并行调用多个异步方法,以及如何修改已经实现异步模式的类,以使用新的关键字。

所有Foundations的示例代码都使用了如下名称空间:

System

System.Collections.Generic

System.IO

System.Linq

System.Net

System.Runtime.CompilerServices

System.Threading

System.Threading.Tasks

注:这个可下载的示例应用程序使用了命令行参数,因为可以轻松地验证每个场景。例如,使用dotnet CLI,可以通过命令:dotnet run ---async传递-async命令行参数。使用Visual Studio,还可以在Debug Project Settings中配置应用程序的参数。

为了更好的理解发生了什么,创建TraceThreadAndTask方法,将线程和任务信息写入控制台。Task.CurrentId返回任务的标识符。Thread.CurrentThread.ManagedThreadId返回当前线程的标识符:

        static void TraceThreandAndTask(string info)
        {
            string taskInfo = Task.CurrentId == null?"no task":"task "+Task.CurrentId;
            System.Console.WriteLine($"{info} in thread {Thread.CurrentThread.ManagedThreadId} and {taskInfo}");
        }

创建任务

下面从同步方法Greeting开始,该方法等待一段时间后,返回一个字符串:

        static string Greeting(string name)
        {
            TraceThreadAndTask($"running {nameof(Greeting)}");
            Task.Delay(3000).Wait();
            return $"Hello, {name}";
        }

定义方法GreetingAsync,可以使方法异步化。基于任务的异步模式指定,在异步方法名后加上Async后缀,并返回一个任务。异步方法GreetingAsync和同步方法Greeting具有相同的输入参数,但是它返回的是Task<string>。Task<string>定义了一个返回字符串的任务。一个比较简单的做法是用Task.Run方法返回一个任务。泛型版本的Task.Run<string>()创建一个返回字符串的任务。由于编译器已经知道实现的返回类型(Greeting返回字符串),因此还可以使用Task.Run()来简化实现代码:

        static Task<string> GreetingAsync(string name)=>
        Task.Run<string>(()=>
        {
            TraceThreadAndTask($"running {nameof(GreetingAsync)}");
            return Greeting(name);
        });

调用异步方法

可以使用await关键字来调用返回任务的异步方法GreetingAsync。使用await关键字需要有用async修饰符声明的方法。在GreetingAsync方法完成前,该方法内的其他代码不会继续执行。但是,启动CallerWithAsync方法的线程可以被重用。该线程没有被阻塞:

        async static void CallerWithAsync()
        {
            TraceThreadAndTask($"started {nameof(CallerWithAsync)}");
            string result = await GreetingAsync("Stephanie");
            System.Console.WriteLine(result);
            TraceThreadAndTask($"ended {nameof(CallerWithAsync)}");
        }

运行应用程序时,可以从第一个输出中看到没有任务。GreetingAsync方法在一个任务中运行,这个任务使用的线程与调用者不同。然后,同步Greeting方法在此任务中运行。当Greeting方法返回时,GreetingAsync方法返回,在等待之后,作用域返回到CallerWithAsync方法中。现在,CallerWithAsync方法在不同的线程中运行。不再有任务了,但是尽管这个方法是从线程1开始的,但是在使用了await线程4之后。await确保在任务完成后继续执行,但现在它使用了另一个线程。这种行为在控制台应用程序和具有同步上下文的应用程序之间是不同的:

started CallerWithAsync in thread 1 and no task
running GreetingAsync in thread 4 and task 1
running Greeting in thread 4 and task 1
Hello, Stephanie
ended CallerWithAsync in thread 4 and no task

如果异步方法的结果不传递变量,也可以直接在参数中使用await关键字。在这里,GreetingAsync方法返回的结果将像前面的代码片段一样等待,但是这一次的结果会直接传给Console.WriteLine方法:

        async static void CallerWithAsync2()
        {
            TraceThreadAndTask($"started {nameof(CallerWithAsync2)}");
            System.Console.WriteLine(await GreetingAsync("Stephanie"));
            TraceThreadAndTask($"ended {nameof(CallerWithAsync2)}");
        }

注:在C# 7中,async修饰符可以用于返回void的方法,或者用于返回一个提供GetAwaiter方法的对象。.NET提供Task和ValueTask类型。通过Windows运行库,还可以使用IAsyncOperation。应该避免给带有void的方法使用async修饰符。

使用Awaiter

可以对任何提供GetAwaiter方法并返回awaiter的对象使用async关键字。awaiter用OnCompleted方法实现INotifyCompletion接口。此方法在任务完成时调用。下面的代码片段不是在任务中使用await,而是使用任务的GetAwaiter方法。Task类的GetAwaiter返回一个TaskAwaiter。使用OnCompleted方法,分配一个在任务完成时调用的本地函数:

        private static void CallerWithAwaiter()
        {
            TraceThreadAndTask($"starting {nameof(CallerWithAwaiter)}");
            TaskAwaiter<string> awaiter= GreetingAsync("Matthias").GetAwaiter();
            awaiter.OnCompleted(OnCompletedAwaiter);
            void OnCompletedAwaiter()
            {
                System.Console.WriteLine(awaiter.GetResult());
                TraceThreadAndTask($"ended (nameof(CallerWithAwaiter))");
            }
        }

运行应用程序,结果类似于你使用await关键字的情形。(自己码的代码运行有些问题,将上面方法CallerWithAwaiter()放入Main()方法中,没有等待执行,运行结果不完全)

解决代码块如下:

        private static ValueTask<string> CallerWithAwaiter()
        {
            TraceThreadAndTask($"starting {nameof(CallerWithAwaiter)}");
            Task<string> task = GreetingAsync("Matthisa");
            TaskAwaiter<string> awaiter= task.GetAwaiter();
             awaiter.OnCompleted(OnCompletedAwaiter);
             return new ValueTask<string>(task);
            void OnCompletedAwaiter()
            {
                System.Console.WriteLine(awaiter.GetResult());
                TraceThreadAndTask($"ended (nameof(CallerWithAwaiter))");
            }
        }

然后在Main()方法中,用await和async关键字调用该方法,输出如下:

starting CallerWithAwaiter in thread 1 and no task
running GreetingAsync in thread 4 and task 1
running Greeting in thread 4 and task 1
Hello, Matthisa
ended (nameof(CallerWithAwaiter)) in thread 4 and no task

 

编译器把await关键字后的所有代码放进OnCompleted方法的代码块中来转换await关键字。 

延续任务

还可以使用Task对象的特性来处理任务的延续。GreetingAsync方法返回一个Task<string>对象。该Task<string>对象包含任务创建的信息,并保存到任务完成。Task类的ContinueWith方法定义了任务完成后就调用的代码。指派给ContinueWith方法的委托接收将已完成的任务作为参数传入,使用Result属性可以访问任务返回的结果:

        private static void CallerWithContinuationTask()
        {
            TraceThreadAndTask($"started {nameof(CallerWithContinuationTask)}");
            var t1 = GreetingAsync("Setphanie");
            t1.ContinueWith(t=>
            {
                string result = t.Result;
                System.Console.WriteLine(result);
                TraceThreadAndTask($"ended {nameof(CallerWithContinuationTask)}");
            });
        }

同步上下文

如果验证方法中使用的线程,会发现CallerWithAsync方法、CallerWithAwaiter方法和CallerWithContinuationTask方法,在方法的不同生命阶段使用了不同的线程。一个线程用于调用GreetingAsync方法,另外一个线程执行await关键字后面的代码,或者继续执行ContinueWIth方法内的代码。

使用一个控制台应用程序,通常不会有什么问题。但是,必须保证在所有应该完成的后台任务完成之前,至少有一个前台线程仍然在运行。示例应用程序调用Console.ReadLine来保证主线程一直在运行,直到按下返回键。

为了执行某些动作,有些应用程序会绑定到指定的线程上(例如,在WPF或Windows应用程序中,只有UI线程才能访问UI元素),这将会是一个问题。

如果使用async和await关键字,当await完成之后,不需要进行任何特别处理,就能访问UI线程。默认情况下,生成的代码会把线程转换到拥有同步上下文的线程中。WPF应用程序设置了DispatcherSynchronizationContext属性,Windows Forms应用程序设置了WIndowsFormsSynchronizationContext属性。Windows应用程序使用WinRTSynchronizationContext。如果调用异步方法的线程分配给了上下文,await完成之后将继续执行。默认情况下,使用了同步上下文。如果不使用相同的同步上下文,则必须调用Task方法ConfigureAwait(continueOnCapturedContext:false)。例如,一个Windows应用程序,其await后面的代码没有用到任何的UI元素,在这种情况下,避免切换到同步上下文会执行得更快。

使用 多个异步方法

在一个异步方法中,可以调用一个或多个异步方法。如何编写代码,取决于一个异步方法的结果是否依赖与另一个异步方法。

1.按顺序调用异步方法

使用await关键字可以调用每个异步方法。在有些情况下,如果一个异步方法依赖另一个异步方法的结果,await关键字就非常有用。在这里,GreetingAsync异步方法的第二次调用完全独立于其第一次调用的结果。这样,如果每个异步方法都不使用await,那么整个MultipleAsyncMethods异步方法将更快地返回结果,如下所示:

        private async static void MultipleAsyncMethods()
        {
            string s1 = await GreetingAsync("Stephanie");
            string s2 = await GreetingAsync("Matthias");
            System.Console.WriteLine($"Finished both methodsd. {Environment.NewLine}"+
            $"Result 1: {s1}{Environment.NewLine} Result 2: {s2}");
        }

2.使用组合器

如果异步方法不依赖与其他异步方法,则每个异步方法都不使用await,而是把每个异步方法的返回结果赋值给Task变量,就会执行得更快。GreetingAsync方法返回Task<string>。这些方法现在可以并行运行了。组合器可以帮助实现这一点。一个组合器可以接受多个同一类型的参数,并返回同一类型的值。多个同一类型的参数被组合成一个参数来传递。Task组合器接受多个Task对象作为参数,并返回一个Task。

示例代码调用Task.WhenAll组合器方法,它可以等待,直到两个任务都完成。

        private async static void MultipleAsyncMethodsWithCombinators1()
        {
            Task<string> t1 = GreetingAsync("Stephanie");
            Task<string> t2 = GreetingAsync("Matthias");
            await Task.WhenAll(t1,t2);
            System.Console.WriteLine($"Finished both methodsd. {Environment.NewLine}"+
            $"Result 1: {t1.Result}{Environment.NewLine} Result 2: {t2.Result}");
        }

Task类定义了WhenAll和WhenAny组合器。从WhenAll方法返回的Task,是在所有传入方法的任务都完成了才会返回Task。从WhenAny方法返回的Task,是在其中一个传入方法的任务完成了就会返回Task。

Task类型的WhenAll方法定义了几个重载版本。如果所有的任务返回相同的类型,那么该类型的数组可用于await返回的结果。GreetingAsync方法返回一个Task<string>,等待返回的结果是一个字符串(string)形式。因此,Task.WhenAll可用于返回一个字符串数组:

        private async static void MultipleAsyncMethodsWithCombinators2()
        {
            Task<string> t1 = GreetingAsync("Stephanie");
            Task<string> t2 = GreetingAsync("Matthias");
            string[] result = await Task.WhenAll(t1,t2);
             System.Console.WriteLine($"Finished both methodsd. {Environment.NewLine}"+
            $"Result 1: {result[0]}{Environment.NewLine} Result 2: {result[1]}");
        }

只有等待的所有任务都完成时某个任务才能继续,WhenAll方法就有实际用途。当调用任务在等待完成的任何任务都完成才能执行操作时,可以使用WhenAny方法。它可以使用任务的结果继续。

使用ValueTasks

C# 7带有更灵活的await关键字;它现在可以等待任何提供GetAwaiter方法的对象。一种可用于等待的新类型是ValueTask。与Task类相反,ValueTask是一个结构。这具有性能优势,因为ValueTask在堆上没有对象。

与异步方法调用相比,Task对象的实际开销是多少?需要异步调用的方法通常比堆上的对象有更多的开销。大多数时候,堆上Task对象的开销是可以忽略的,但并不总是这样。例如,某个方法可以有一个路径,其中数据是从一个具有异步API的服务中检索出来的。通过这种数据检索,数据就写入到本地缓存中。第二次调用该方法时,可以以快速的方式检索数据,而不需要创建Task对象。

示例方法GreetingValueTaskAsync正是这样做的。如果该名称已存在于字典中,则结果返回为ValueTask。如果名称不在字典中,将调用GreetingAsync方法,该方法返回一个Task。在此任务中等待检索结果时,将再次返回ValueTask:

        async static ValueTask<string> GreetingValueTaskAsync(string name)
        {
            if(names.TryGetValue(name,out string result))
            {
                return result;
            }
            else
            {
                result = await GreetingAsync(name);
                names.Add(name,result);
                return result;
            }
        }

UseValueTask方法使用相同的名称调用GreetingValueTaskAsync方法两次。第一次使用GreetingAsync方法检索数据;第二次,数据在字典中找到并从那里返回:

        async static void UseValuTask()
        {
            string result = await GreetingValueTaskAsync("Katharina");
            System.Console.WriteLine(result);
            string result2 = await GreetingValueTaskAsync("Katharina");
            System.Console.WriteLine(result2);
        }

如果方法不使用async修饰符,而需要返回ValueTask,就可以使用传递结果或者传递Task对象的构造函数创建ValueTask对象:

        static ValueTask<string> GreetingValueTask2Async(string name)
        {
            if(names.TryGetValue(name,out string result))
            {
                return new ValueTask<string>(result);
            }
            else
            {
                Task<string> t1 = GreetingAsync(name);
                TaskAwaiter<string> awaiter= t1.GetAwaiter();
                awaiter.OnCompleted(OnCompletion);
                return new ValueTask<string>(t1);
                void OnCompletion()
                {
                    names.Add(name,awaiter.GetResult());
                }
            }
        }

转换异步模式

并非.NET Framework的所有类都引入了新的异步方法。在使用框架中的不同类时会发现,还有许多类只提供了BeginXXX方法和EndXXX方法的异步模式,没有提供基于任务的异步模式。但是,可以把异步模式转换为基于任务的异步模式。

这个示例使用HttpWebRequest类和BeginGetResponse方法将该方法转换为基于任务的异步模式,Task.Factory.FromAsync是一个泛型方法,它提供了一些重载版本,将异步模式转换为基于任务的异步模式。对于示例应用程序,当调用HttpWebRequest的BeginGetResponse方法时,将发出异步网络请求。这个方法返回一个IAsyncResult,它是FromAsync方法的第一个参数。第二个参数是对EndGetResponse方法的引用,它需要一个带有IAsyncResult参数(即EndGetResponse方法)的委托。第二个参数还需要返回WebResponse,由FromAsync方法的泛型参数决定。当IAsyncResult信号完成时,任务助手功能会调用EndGetResponse方法:

        async static void ConvertingAsyncPattern()
        {
            HttpWebRequest request = WebRequest.Create("http://www.microsoft.com") as HttpWebRequest;
            using(WebResponse response = await Task.Factory.FromAsync<WebResponse>(
                request.BeginGetResponse(null,null),request.EndGetResponse))
            {
                Stream stream = response.GetResponseStream();
                using(var reader = new StreamReader(stream))
                {
                    string content = reader.ReadToEnd();
                    System.Console.WriteLine(content.Substring(0,1000));
                }
            }
        }

警告:在旧应用程序中,通常在使用异步模式时使用委托的BeginInvoke()方法。在.NET Core应用程序中使用此方法时,编译器不会报错。但是,在运行时,将抛出一个平台不支持的异常。

发布了87 篇原创文章 · 获赞 1 · 访问量 2120
展开阅读全文
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 技术工厂 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览