• 《C#深入理解3册》async和await异步编程


    15.2 思考异步编程

    15.2.1 异步执行的基础

      实际上, C#编译器会对所有await都构建一个后续操作。这个理念表述起来非常简单,显然是为了可读性和开发者的健康。

      实际上基于任务的异步模式要稍有不同。它并不会将后续操作传递给异步操作,而是在异步操作开始时返回一个token,我们可以用这个token在稍后提供后续操作。它表示正在进行的操作,在返回调用代码前可能已经完成,也可能正在处理。token用于表达这样的想法:在这个操作完成之前,不能进行下一步处理。 token的形式通常为TaskTask<TResult>,但这并不是必须的。

    在C# 5中,异步方法的执行流通常遵守下列流程。
    (1) 执行某些操作。
    (2) 开始异步操作,并记住返回的token
    (3) 可能会执行其他操作。(在异步操作完成前,往往不能进行任何操作,此时忽略该步骤。)
    (4) 等待异步操作完成(通过token)。
    (5) 执行其他操作。
    (6) 完成。

    同步上下文

    之前我提到过, UI代码的金科玉律之一是,除非在正确的线程中,否则不要更新用户界面。
    在“检查页面长度”的示例中(代码清单15-1) ,我们需要确保await表达式之后的代码在UI线程上的执行。异步函数能够回到正确的线程中,是因为使用了SynchronizationContext类 。 该 类 早 在 .NET 2.0 中 就 已 存 在 , 用 以 供 BackgroundWorker 等 其 他 组 件 使 用 。

    SynchronizationContext涵盖了“在适当的线程上”执行委托这一理念。其Post(异步)和Send(同步)消息的方法,与Windows Forms中的Control.BeginInvoke和Control.Invoke异曲同工。

    不同的执行环境使用不同的上下文。例如,某个上下文可能会从线程池中取出一个线程并执行给定的行为。除了同步上下文以外还有很多上下文信息,但如果你想知道异步方法是如何在正确的位置上执行的,就要牢记同步上下文。

    要了解更多关于SynchronizationContext的信息,请阅读Stephen Cleary在MSDN杂志上关于该话题的文章(http://mng.bz/5cDw) 。如果你是ASP.NET开发者的话,应尤其注意:
    ASP.NET上下文会让看上去没问题的代码死锁,轻易地让粗心的开发者掉进陷阱。

    15.2.2 异步方法

    我们主要感兴趣的是异步方法本身,但也包含了其他方法,这样就能看到它们是如何交互的。
    特别是,你一定要了解方法边界处的有效类型。

    15.3 语法和语义

    15.3.1 声明异步方法

    异步方法的声明语法与其他方法完全一样,只是要包含async上下文关键字。async可以出现在返回类型之前的任何位置。以下这些都是有效的:

    public static async Task<int> FooAsync() { ... }
    public async static Task<int> FooAsync() { ... }
    async public Task<int> FooAsync() { ... }
    public async virtual Task<int> FooAsync() { ... }
    

    async上下文关键字有一个不为人知的秘密:对语言设计者来说,方法签名中有没有该关键字都无所谓。3

    async修饰符在生成的代码中没有作用,这个事实是非常重要的。对调用方法来说,它只是一个可能会返回任务的普通方法。你可以将一个(具有适当签名的)已有方法改成使用async,反之亦然。对于源代码和二进制来说,这都是一个兼容的转换。

    15.3.2 异步方法的返回类型

      调用者和异步方法之间是通过返回值来通信的。异步函数的返回类型只能为:

    • void;
    • Task;
    • Task<TResult>(某些类型的TResult,其自身即可为类型参数)。

      1.在某种意义上,你可以认为Task就是Task<void>类型,如果这么写合法的话。

      2.对于一个异步方法,只有在作为事件订阅者时才应该返回void。在其他不需要特定返回值的情况下,最好将方法声明为返回Task。这样,调用者可以等待操作完成,以及探测失败情况等。

      3.还有一个关于异步方法签名的约束:所有参数都不能使用out或ref修饰符。因为这些修饰符是用于将通信信息返回给调用代码的;而且在控制返回给调用者时,某些异步方法可能还没有开始执行,因此引用参数可能还没有赋值。

    15.3.3 可等待模式

      await表达式非常简单,只是在其他表达式前面加了一个await。当然,对于能等待的东西是有限制的。需要提醒的是,我们正在谈论图15-1的第二个边界,即异步方法如何与其他异步操作交互。一般来说,我们只能等待(await)一个异步操作。换句话说,是包含以下含义的操作:
     告知是否已经完成;
     如未完成可附加后续操作;
     获取结果,该结果可能为返回值,但至少可以指明成功或失败。

    15.3.6 异常

    1.在等待时拆包异常

    在等待时拆包异常awaiterGetResult方法可获取返回值(如果存在的话) ;同样地,如果存在异常,它还负责将异常从异步操作传递回方法中。听上去简单做起来难,因为在异步世界里,单个Task可表示多个操作,并导致多个失败。尽管还存在其他的可等待模式实现,但有必要专门介绍Task,因为在大多数情况下,我们等待的都是这个类型。

    Task有多种方式可以表示异常

    • 当异步操作失败时,任务的Status变为Faulted(并且IsFaulted返回true)。

    • Exception属性返回一个AggregateException,该AggregateException包含所有(可能有多个)造成任务失败的异常;如果任务没有错误,则返回null。

    • 如果任务的最终状态为错误,则Wait()方法将抛出一个AggregateException。

    • Task<T>Result属性(同样等待完成)也将抛出AggregateException。

    取消操作

      此外,任务还支持取消操作,可通过CancellationTokenSourceCancellationToken来实现这一点。如果任务取消了,Wait()方法和Result属性都将抛出包含OperationCanceledException的AggregateException(实际上是一个TaskCanceledException,它继承自OperationCanceledException),但状态将变为Canceled,而不是Faulted

    抛出第一个异常

      在等待任务时,任务出错或取消都将抛出异常,但并不是AggregateException。大多情况下为方便起见,抛出的是AggregateException中的第一个异常,往往这就是我们想要的。
      异步特性就是像编写同步代码那样编写异步代码,如下所示:

    async Task<string> FetchFirstSuccessfulAsync(IEnumerable urls)
    {
        // TODO:验证是否获取到了URL
        foreach (string url in urls)
        {
            try
            {
                using (var client = new HttpClient())
                {
                   return await client.GetStringAsync(url);
                }
            }
            catch (WebException exception)
            {
                 // TODO:记录日志、更新统计信息等
            }
        }
        throw new WebException("No URLs succeeded");
    }
    

      但GetStringAsync()方法不能为服务器超时等错误抛出WebException,因为方法仅仅启动了操作。它只能返回一个包含WebException的任务 。 如 果 简 单 地 调 用 该 任 务 的 Wait() 方 法 , 将 会 抛 出 一 个 包 含 WebException 的AggregateException。任务awaiter的GetResult方法将抛出WebException,并被以上代码所捕获。

      当然,这样会丢失信息。如果错误的任务中包含多个异常,则GetResult只能抛出其中的一个异常(即第一个)。你可能需要重写以上代码,这样在发生错误时,调用者就可捕获AggregateException并检查所有失败的原因。重要的是,一些框架方法(如Task.WhenAll())也可以实现这一点。 WhenAll()方法可异步等待(方法调用中指定的)多个任务的完成。如果其中有失败的,则结果即为失败,并包含所有错误任务中的异常。但如果只是等待(await)WhenAll()返回的任务,则只能看到第一个异常。

      幸好,要解决这个问题并不需要太多的工作。我们可以使用可等待模式的知识,编写一个Task的扩展方法,从而创建一个可从任务中抛出原始AggregateException的特殊可等待模式成员。

    public static partial class TaskExtensions
        {
            public static AggregatedExceptionAwaitable WithAggregatedExceptions(this Task task)
            {
                if (task == null)
                {
                    throw new ArgumentNullException("task");
                }
    
                return new AggregatedExceptionAwaitable(task);
            }
    
            public struct AggregatedExceptionAwaitable
            {
                private readonly Task task;
    
                internal AggregatedExceptionAwaitable(Task task)
                {
                    this.task = task;
                }
    
                public AggregatedExceptionAwaiter GetAwaiter()
                {
                    return new AggregatedExceptionAwaiter(task);
                }
            }
    
            public struct AggregatedExceptionAwaiter : ICriticalNotifyCompletion
            {
                private readonly Task task;
    
                internal AggregatedExceptionAwaiter(Task task)
                {
                    this.task = task;
                }
    
                // Delegate most members to the task's awaiter
                public bool IsCompleted { get { return task.GetAwaiter().IsCompleted; } }
    
                public void UnsafeOnCompleted(Action continuation)
                {
     	            task.GetAwaiter().UnsafeOnCompleted(continuation);		//❶ 委托给任务awaiter
                }
    
                public void OnCompleted(Action continuation)
                {
                    task.GetAwaiter().OnCompleted(continuation);		//❶ 委托给任务awaiter
                }
    
                public void GetResult()
                {
                    // This will throw AggregateException directly on failure,
                    // unlike task.GetAwaiter().GetResult()
                    task.Wait();		//❷ 发生错误时,直接抛出AggregateException
                }
            }
        }
    
        class AggregatedExceptions
        {
            static void Main()
            {
                MainAsync().Wait();
                Console.ReadKey();
            }
    
            private async static Task MainAsync()
            {
                Task task1 = Task.Run(() => { throw new Exception("Message 1"); });
                Task task2 = Task.Run(() => { throw new Exception("Message 2"); });
    
                try
                {
                    await Task.WhenAll(task1, task2);
                }
    
                catch (Exception e)
                {
                    Console.WriteLine("Caught {0}", e.Message);
                }
    
                try
                {
                    await Task.WhenAll(task1, task2).WithAggregatedExceptions();
                }
                catch (AggregateException e)
                {
                    Console.WriteLine("Caught {0} exceptions: {1}", e.InnerExceptions.Count,
                                      string.Join(", ", e.InnerExceptions.Select(x => x.Message)));
                }
            }
        }
    
    //output:
    //Caught Message 1
    //Caught 2 exceptions: Message 1, Message 2
    

      Task<T>也需要一个类似的方法,即在GetResult()中使用return task.Result,而不是调用Wait()。重点在于,我们把自己不想处理的部分委托给了任务的awaiter❶ ,而回避了GetResult()的常规行为,即对异常进行拆包。在调用GetResult时,我们知道该任务处于即将结束的状态,因此Wait()调用❷可立即返回,这并不妨碍我们要实现的异步性。

      WithAggregateException()返回自定义的可等待模式成员,而后者的GetAwaiter()又提供自定义的awaiter,并支持C#编译器所需要的操作来等待结果。注意,也可将可等待模式成员和awaiter合并,并没有要求二者必须是不同类型,但分开的话会感觉更清晰一些。

    2.在抛出异常时进行包装

    异步方法在调用时永远不会直接抛出异常。

    异常方法会返回TaskTask<T>,方法内抛出的任何异常(包括从其他同步或异步操作中传播过来的异常)都将简单地传递给任务,就像前面介绍的那样。如果调用者直接等待①任务,则可得到一个包含真正异常的AggregateException;但如果调用者使用await,异常则会从任务中解包。返回void的异步方法可向原始的SynchronizationContext报告异常,如何处理将取决于上下文②。

    //以熟悉的方式处理异步的异常
    
    static async Task MainAsync()
    {
        Task<string> task = ReadFileAsync("garbage file");   //❶ 开始异步读取
        try
        {
    	    //任务中解包
            string text = await task;     //❷ 等待内容
                Console.WriteLine("File contents: {0}", text);
        }
        catch (IOException e)    //❸ 处理IO失败
        {
            Console.WriteLine("Caught IOException: {0}", e.Message);
        }
    }
    
    static async Task<string> ReadFileAsync(string filename)
    {
        using (var reader = File.OpenText(filename))    //❹ 同步打开文件
        {
            return await reader.ReadToEndAsync();
        }
    }
    

    调用File.OpenText时可抛出一个IOException❹ (除非创建了一个名为“ garbage file”的文件), 但如果ReadToEndAsync返回的任务失败了,也会出现同样的执行路径。在MainAsync中, ReadFileAsync的调用❶ 发生在进入try块之前,但只有在等待任务时 ❷,调用者才能看到异常并在catch块中捕获 ❸,就像前面的WebException示例一样。同样,除异常发生的时机以外,其行为我们也非常熟悉。

    来看AggregateException的情况:

    //
    		static void Main()
            {
                MainAsync().Wait();
            }
    
            static async Task MainAsync()
            {
                Task<string> task = ReadFileAsync("garbage file");
                try
                {
                    // task.Wait() 或者 task.Result 
                    string text =  task.Result;
                    Console.WriteLine("File contents: {0}", text);
                }
                catch (AggregateException e)
                {
                     Console.WriteLine("Caught {0} exceptions: {1}", e.InnerExceptions.Count,
                                      string.Join(", ", e.InnerExceptions.Select(x => x.Message)));	//执行这里
                }
                catch (IOException e)
                {
                    Console.WriteLine("Caught IOException: {0}", e.Message);
                }
    
            }
    
            static async Task<string> ReadFileAsync(string filename)
            {
                using (var reader = File.OpenText(filename))
                {
                    return await reader.ReadToEndAsync();
                }
            }
    
    //Caught 1 exceptions: 未能找到文件“E:OtherChaptersChapter15inDebuggarbage file”。
    

      迭代器块类似,参数验证会有些麻烦。假设我们在验证完参数不含有空值后,想在异步方法里做一些处理。如果像在同步代码中那样验证参数,那么在等待任务之前,调用者不会得到任何错误提示。

    //异步方法中失效的参数验证
    
    static async Task MainAsync()
    {
        Task<int> task = ComputeLengthAsync(null);  //故意传入错误的参数
        Console.WriteLine("Fetched the task");
        int length = await task;   //❶ 等待结果
        Console.WriteLine("Length: {0}", length);
    }
    static async Task<int> ComputeLengthAsync(string text)
    {
    	if (text == null)
        {
            throw new ArgumentNullException("text");   // ❷  立即抛出异常
        }
        await Task.Delay(500);  //模拟真实的异步工作
        return text.Length;
    }
    

      实际上,在输出这条结果之前,异常就已经同步地抛出了,这是因为在验证语句之前并不存在await表达式 。但调用代码直到等待返回的任务时 ,才能看到这个异常。

    在C# 5中,有两种方式可以迫使异常立即抛出。

    //将参数验证从异步实现中分离出来
    
    static Task<int> ComputeLengthAsync(string text)
    {
        if (text == null)
        {
            throw new ArgumentNullException("text");
        }
        return ComputeLengthAsyncImpl(text);
    }
    static async Task<int> ComputeLengthAsyncImpl(string text)
    {
        await Task.Delay(500); // 模拟真正的异步工作
        return text.Length;
    }
    

    3.取消处理

      任务并行库(TPL)利用CancellationTokenSourceCancellationToken两种类型向.NET 4中引入了一套统一的取消模型。该模型的理念是,创建一个CancellationTokenSource,然后向其请求一个CancellationToken,并传递给异步操作。可在source上只执行取消操作,但该操作会反映到token上。(这意味着你可以向多个操作传递相同的token,而不用担心它们之间会相互干扰。)取消token有很多种方式,最常用的是调用ThrowIfCancellationRequested,如果取消了token,并且没有其他操作,则会抛出OperationCanceledException。如果在同步调用(如Task.Wait)中执行了取消操作,则可抛出同样的异常。

    //通过抛出OperationCanceledException来创建一个取消的任务
    
    		static void Main()
            {
                Task task = ThrowCancellationException();
                Console.WriteLine(task.Status);
            }
    
            static async Task ThrowCancellationException()
            {
                throw new OperationCanceledException();
            }
    
    //output:
    //Canceled
    

      这段代码的输出为Canceld,而不是Faulted。如果在任务上执行Wait(),或请求其结果(针对Task<T>) ,则AggregateException内还是会抛出异常,所以没有必要在每次使用任务时都显式检查是否有取消操作。

      重要的是,等待一个取消了的操作,将抛出原始的OperationCanceledException。这意味着如果不采取一些直接的行动,从异步方法返回的任务同样会被取消,因为取消操作具有可传播性。

    //通过一个取消的延迟操作来取消异步方法
    		static void Main()
            {
                var source = new CancellationTokenSource();
                var task = DelayFor30Seconds(source.Token);
                source.CancelAfter(TimeSpan.FromSeconds(1));
                Console.WriteLine("Initial status: {0}", task.Status);
                try
                {
                    task.Wait();
                }
                catch (AggregateException e)
                {
                    Console.WriteLine("Caught {0}", e.InnerExceptions[0]);
                }
                Console.WriteLine("Final status: {0}", task.Status);
            }
    
            static async Task DelayFor30Seconds(CancellationToken token)
            {
                Console.WriteLine("Waiting for 30 seconds...");
                await Task.Delay(TimeSpan.FromSeconds(30), token);
            }
    
    //Waiting for 30 seconds...
    //Initial status: WaitingForActivation
    //Caught System.Threading.Tasks.TaskCanceledException: 已取消一个任务。
    //Final status: Canceled
    

      代码中启动了一个异步操作 ,该操作调用Task.Delay模拟真正的工作 ,并提供了一个CancellationToken。这一次,我们的确涉及了多个线程:到达await表达式时,控制返回到调用方法,这时要求CancellationToken在1秒后取消 。然后(同步地)等待任务完成 ,并期望在最终得到一个异常。最后展示任务的状态。

      可认为取消操作默认是可传递的:如果A操作等待B操作,而B操作被取消了,那么我们认为A操作也被取消了。

      当然,你不必这么做。你可以在DelayFor30Seconds方法中捕获OperationCanceledException,然后或继续做其他事情,或立即返回,或干脆抛出一个其他类型的异常。异步特性不会移除控制,它只是提供了一种有用的默认行为而已。

  • 相关阅读:
    安装完MySQL数据库设置密码
    pom.xml
    性能测试更像一次科学实验
    gitlab git
    postman
    python3 session cookie
    自动化测试的概念及工具
    项目启动加载配置,以及IP黑名单,使用CommandLineRunner和ApplicationRunner来实现(一般用在网关进行拦截黑名单)
    使用JWT登录生成token
    国际化的实现i18n--错误码国际化以及在springboot项目中使用
  • 原文地址:https://www.cnblogs.com/tangge/p/14726638.html
Copyright © 2020-2023  润新知