• 异步函数


    异步函数

     

    此入门教程是记录下方参考资料视频的过程
    开发工具:Visual Studio 2019

    参考资料:https://www.bilibili.com/video/BV1Zf4y117fs

    目录

    C# 异步编程基础(一)线程和阻塞

    C# 异步编程基础(二)线程安全、向线程传递数据和异常处理

    C# 异步编程基础(三)线程优先级、信号和线程池

    C# 异步编程基础(四) 富客户端应用程序的线程 和 同步上下文 Synchronization Contexts

    C# 异步编程基础(五)Task

    C# 异步编程基础(六)Continuation 继续/延续 、TaskCompletionSource、实现 Task.Delay

    C# 异步编程基础(七)异步原理

    C# 异步编程基础(八) 异步函数

    C# 异步编程基础(九) 异步中的同步上下文、ValueTask

    C# 异步编程基础(十) 取消(cancellation)、进度报告、TAP(Task-Based Asynchronous Pattern)、Task组合器

    异步函数

    async和await关键字可以让你写出和同步代码一样简洁且结构相同的异步代码

    await

    1. await关键字简化了附加continuation的过程
    2. 其结构如下:
        var result=await expression;
        statement(s);
    
    1. 它的作用相当于:
    var awaiter=expression.GetAwaiter();
    awaiter.OnCompleted(()=>
    {
        var result=awaiter.GetResult();
        statement(s);
    });
    

    例子

    static async Task Main(string[] args)
    {
    
    }
    //使用await的函数一定要async修饰
    //await不能调用无返回值的函数
    static async Task DisplayPrimesCountAsync()
    {
        int result = await GetPrimesCountAsync(2, 1000000);
        Console.WriteLine(result);
    }
    
    static Task<int> GetPrimesCountAsync(int start, int count)
    {
        return Task.Run(() =>
        ParallelEnumerable.Range(start, count).Count(n =>
             Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
    }
    

    async修饰符

    1. async修饰符会让编译器把await当作关键字而不是标识符(C# 5 以前可能会使用await作为标识符)
    2. async修饰符只能应用于方法(包括lambda表达式)
      该方法可返回void、Task、Task
    3. async修饰符对方法的签名或public元数据没有影响(和unsafe一样),它只会影响方法内部
      在接口内使用async是没有意义的
      使用async来重载非async的方法却是合法的(只要方法签名一致)
    4. 使用了async修饰符的方法就是“异步函数”

    异步方法如何执行

    1. 遇到await表达式,执行(正常情况下)会返回调用者
      就像iterator里面的yield return
      在返回前,运行时会附加一个continuation到await的task
         为了保证task结束时,执行会跳回原方法,从停止的地方继续执行
      如果发生故障,那么异常就会被重新抛出
      如果一切正常,那么它的返回值就会赋值给await表达式

    例子

    static async Task Main(string[] args)
    {
    
    }
    //两种方法作用相同
    static void DisplayPrimesCount()
    {
        var awaiter = GetPrimesCountAsync(2, 1000000).GetAwaiter();
        awaiter.OnCompleted(() =>
        {
            int result = awaiter.GetResult();
            Console.WriteLine(result);
        });
    }
    
    static async Task DisplayPrimesCountAsync()
    {
        int result = await GetPrimesCountAsync(2, 1000000);
        Console.WriteLine(result);
    }
    
    static Task<int> GetPrimesCountAsync(int start, int count)
    {
        return Task.Run(() =>
        ParallelEnumerable.Range(start, count).Count(n =>
                Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
    }
    

    可以await什么?

    1. 你await的表达式通常是一个task
    2. 也可以满足下列条件的任意对象:
      有GetAwaiter方法,它返回一个awaiter(实现了INotifyCompletion.OnCompleted接口)
      返回适当类型的GetResult方法
      一个bool类型的IsCompleted属性

    捕获本地状态

    1. await表达式最牛之处就是它几乎可以出现在任何地方
    2. 特别的,在异步方法内,await表达式可以替换任何表达式,除了lock表达式和unsafe上下文

    例子

    static async Task Main(string[] args)
    {
    
    }
    static async void DisplayPrimeCounts()
    {
        for (int i = 0; i < 10; i++)
        {
            Console.WriteLine(await GetPrimesCountAsync(i * 1000000 + 2, 1000000));
        }
    }
    static Task<int> GetPrimesCountAsync(int start, int count)
    {
        return Task.Run(() =>
        ParallelEnumerable.Range(start, count).Count(n =>
                Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
    }
    

    await之后在哪个线程上执行

    1. 在await表达式之后,编译器依赖于continuation(通过awaiter模式)来继续执行
    2. 如果在富客户端的UI线程上,同步上下文会保证后续是在原线程上执行
    3. 否则,就会在task结束的线程上继续执行

    UI上的await

    1. 例子,建议这样写异步函数
    public MainWindow()
    {
        InitializeComponent();
    
    }
    
    async void Go()
    {
        this.Button1.IsEnabled = false;
    
        for (int i = 1; i < 5; i++)
        {
            this.TextMessage.Text += await this.GetPrimesCountAsync(i * 1000000, 1000000) + " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1) + Environment.NewLine;
        }
    
        this.Button1.IsEnabled = true;
    }
    
    Task<int> GetPrimesCountAsync(int start, int count)
    {
        return Task.Run(() =>
            ParallelEnumerable.Range(start, count).Count(n =>
                Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
    }
    
    private void Button1_Click(object sender, RoutedEventArgs e)
    {
        this.TextMessage.Text = null;
        this.Go();
    }
    
    1. 本例中,只有GetPeimesCountAsync中的代码在worker线程上运行
    2. Go中的代码会“租用”UI线程上的时间
    3. 可以说,Go是在消息循环中“伪并发”的执行
      也就是说:它和UI线程处理的其它时间是穿插执行的
      因为这种伪并发,唯一能发生“抢占”的时刻就是在await期间,这其实简化了线程安全,防止重新进入即可
    4. 这种并发发生在调用栈较浅的地方(Task.Run调用的代码里)
    5. 为了从该模型获益,真正的并发代码要避免访问共享状态或UI控件

    例子

    async void Go()
    {
        this.Button1.IsEnabled = false;
    
        string[] urls = "www.bing.com www.baidu.com www.cnblogs.com".Split();
        int totalLength = 0;
        try
        {
            foreach (string url in urls)
            {
                var uri = new Uri("http://" + url);
                byte[] data = await new WebClient().DownloadDataTaskAsync(uri);
                this.TextMessage.Text += "Length of " + url + " is " + data.Length + Environment.NewLine;
                totalLength += data.Length;
            }
            this.TextMessage.Text += "Total length " + totalLength;
        }
        catch (WebException e)
        {
            this.TextMessage.Text += "Error:" + e.Message;
        }
        finally
        {
            this.Button1.IsEnabled = true;
        }
    
    }
    
    private void Button1_Click(object sender, RoutedEventArgs e)
    {
        this.TextMessage.Text = null;
        this.Go();
    }
    

    伪代码:

    为本线程设置同步上下文(WPF)
    while(!程序结束)
    {
        等着消息队列中发生一些事情
        发生了事情,是哪种消息?
        键盘/鼠标消息->触发event handler
        用户BeginInvoke/Invoke 消息->执行委托
    }
    

       附加到UI元素的event handler通过消息循环执行
       因为在UI线程上await,continuation将消息发送到同步上下文上,该同步上下文通过消息循环执行,来保证整个Go方法伪并发的在UI线程上执行

    与粗粒度的并发相比

    1、例如使用BackgroundWorker,不推荐这样写异步函数

    void Go()
    {
        for (int i = 1; i < 5; i++)
        {
            int result = this.GetPrimesCount(i * 1000000, 1000000);
            this.Dispatcher.BeginInvoke(new Action(() =>
            this.TextMessage.Text += result + " primes between " + (i * 1000000) + " and " + ((i + 1) * 1000000 - 1) + Environment.NewLine));
        }
        this.Dispatcher.BeginInvoke(new Action(() => this.Button1.IsEnabled = true));
    }
    
    int GetPrimesCount(int start, int count)
    {
        return ParallelEnumerable.Range(start, count).Count(n =>
                Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0));
    }
    
    private async void Button1_Click(object sender, RoutedEventArgs e)
    {
        this.TextMessage.Text = null;
        this.Button1.IsEnabled = false;
    
        Task.Run(() => this.Go());
    }
    
    1. 整个同步调用图都在worker线程上
    2. 必须在代码中到处使用Dispatcher.BeginInvoke
    3. 循环本身在worker线程上
    4. 引入了race condition
    5. 若实现取消和过程报告,会使得线程安全问题更任意发生,在方法中新添加任何的代码也是同样的效果

    编写异步函数

    1. 对于任何异步函数,你可以使用Task替代void作为返回类型,让该方法成为更有效的异步(可以进行await)
      例子
    static async Task Main(string[] args)
    {
        //不加await关键字就是并行,不会等待
        await PrintAnswerToLife();
    }
    static async Task PrintAnswerToLife()
    {
        await Task.Delay(5000);
        int answer = 21 * 2;
        Console.WriteLine(answer);
    }
    
    1. 并不需要在方法体中显式的返回Task。编译器会生成一个Task(当方法完成或发生异常时),这使得创建异步的调用链非常方便
      例子
    static async Task Main(string[] args)
    {
    }
    static async Task Go()
    {
        await PrintAnswerToLife();
        Console.WriteLine("Done");
    }
    static async Task PrintAnswerToLife()
    {
        await Task.Delay(5000);
        int answer = 21 * 2;
        Console.WriteLine(answer);
    }
    
    1. 编译器会对返回Task的异步函数进行扩展,使其成为当发送信号或发生故障时使用TaskCompletionSource来创建Task的代码
      大致代码
    static Task PrintAnswerToLide()
    {
        var tcs = new TaskCompletionSource<object>();
        var awaiter = Task.Delay(5000).GetAwaiter();
        awaiter.OnCompleted(() =>
        {
            try
            {
                awaiter.GetResult();
                int answer = 21 * 2;
                Console.WriteLine(answer);
                tcs.SetResult(null);
            }
            catch (Exception e)
            {
                tcs.SetException(e);
            }
        });
        return tcs.Task;
    }
    
    1. 因此,当返回Task的异步方法结束的时候,执行就会跳回到对它进行await的地方(通过continuation)

    编写异步函数,富客户端场景下

    1. 富客户端场景下,执行在此刻会跳回到UI线程(如果目前不在UI线程的话)
    2. 否则,就在continuation返回的任意线程上继续执行
    3. 这意味着,在异步调用图中向上冒泡的时候,不会发生延迟成本,除非是UI线程启动的第一次“反弹”

    返回Task

    1. 如果方法体返回TResult,那么异步方法就可以返回Task
      例子
    static async Task Main(string[] args)
    {
    
    }
    static async Task<int> GetAnswerToLiife()
    {
        await Task.Delay(5000);
        int answer = 21 * 2;
        return answer;
    }
    
    1. 其原理就是给TaskCompletion发送的信号带有值,而不是null
      例子
    static async Task PrintAnswerToLife()
    {
        int answer = await GetAnswerToLife();
        Console.WriteLine(answer);
    }
    static async Task<int> GetAnswerToLife()
    {
        await Task.Delay(5000);
        int answer = 21 * 2;
        return answer;
    }
    
    1. 与同步编程很相似,是故意这样设计的
      同步版本
    static void Main(string[] args)
    {
    
    }
    static void Go()
    {
        PrintAnswerToLife();
        Console.WriteLine("Done");
    }
    static void PrintAnswerToLife()
    {
        int answer = GetAnswerToLife();
        Console.WriteLine(answer);
    }
    static int GetAnswerToLife()
    {
        Thread.Sleep(5000);
        int answer = 21 * 2;
        return answer;
    }
    

    C#中如何设计异步函数

    1. 以同步的方式编写方法
    2. 使用异步调用来替代同步调用,并且进行await
    3. 除了顶层方法外(UI控件的event handler,因为没有await调用),把你方法的返回类型升级为Task或Task,这样它们就可以进行await了

    编译器能对异步函数生成Task意味着什么?

    1. 大多数情况下,你只需要在初始化IO-Bound并发的底层方法里显式的初始化TaskCompletionSource,这种情况很少见
    2. 针对初始化Compute-Bound的并发方法,你可以使用Task.Run来创建Task

    异步调用图执行

    例子

    static async Task Main(string[] args)
    {
        //Main Thread
        await Go();
    }
    static async Task Go()
    {
        var task = PrintAnswerToLife();
        await task;
        Console.WriteLine("Done");
    }
    static async Task PrintAnswerToLife()
    {
        var task = GetAnswerToLife();
        int answer = await task;
        Console.WriteLine(answer);
    }
    static async Task<int> GetAnswerToLife()
    {
        var task = Task.Delay(5000);
        await task;
        int answer = 21 * 2;
        return answer;
    }
    
    1. 整个执行与之前同步例子中调用图的执行顺序是一样的,因为我们对每个异步函数的调用都进行了await
    2. 在调用图中创建了一个没有并行和重叠的连续流
    3. 每个await在执行中都创建了一个间隙,在间隙后,程序可以从中断处恢复执

    并行(Parallelism)

    1. 不使用await来调用异步函数会导致并行执行的发生
    2. 例如:_button.Click+=(sender,args)=>Go();
      主线程仍然在执行,GO()也在执行
      确实也能满足保持UI响应的并发要求
    3. 同样,可以并行跑两个操作:
        var task1=PrintAnswerToLife();
        var task2=PrintAnswerToLife();
        await task1;
        await task2;
    

    异步Lambda表达式

    1. 匿名方法(包括Lambda表达式),通过使用async也可以变成异步方法
    2. 调用方式也一样
    static async Task Main(string[] args)
    {
        Func<Task> unnamed = async () =>
        {
            await Task.Delay(1000);
            Console.WriteLine("Foo");
        };
    
        await NamedMethod();
        await unnamed();
    }
    static async Task NamedMethod()
    {
        await Task.Delay(1000);
        Console.WriteLine("Foo");
    }
    
    1. 附加event handler的时候也可以使用异步Lambda表达式

    例子

    myButton.Click+=async (sender,args)=>
    {
        await Task.Delay(1000);
        myButton.Content="Done";
    }
    

    相当于

    myButton.Click+=ButtonHandler;
    
    async void ButtonHandler(object sender,EventArgs args)
    {
        await Task.Delay(1000);
        myButton.Content="Done";
    }
    
    1. 也可以返回Task
    static async Task Main(string[] args)
    {
        Func<Task<int>> unnamed = async () =>
        {
            await Task.Delay(1000);
            return 123;
        };
        int answer = await unnamed();
    }
    

    异步函数 结束

  • 相关阅读:
    Jetpack MVVM 高频提问和解答
    Android-Jetpack架构组件-—-Navigation#
    Jetpack明星组件 —ViewModel #
    Android开发把 LiveData 用于事件传递那些坑#
    Android官方架构组件Lifecycle#
    Android架构组件JetPack之Lifecycle#
    Android架构组件JetPack之LiveData的工作原理#
    DataBinding从入门到通透#
    SpringBoot 分包方式多数据源
    SpringBoot与数据访问
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/16521250.html
Copyright © 2020-2023  润新知