• C#语言async, await 简单介绍与实例(入门级)


          本文介绍异步编程的基本思想和语法。在程序处理里,程序基本上有两种处理方式:同步和异步。对于有些新手,甚至认为“同步”是同时进行的意思,这显然是错误的。

    同步的基本意思是:程序一个个执行方法,或者说在方法调用上,fun1(), fun2(), fun3(),fun4().. 按顺序调用,而异步的意思是:方法不是按顺序执行,可能fun2执行的时间比较长

    那就先执行fun3,fun4。等执行完了fun2 在执行后面的fun1,fun6,fun7..., 很显然,异步编程比同步编程复杂很多,因为他涉及到线程的同步。

     

          注意:对于单核CPU来说,任一时刻只能执行一条指令,对于这种微观观点,我们不用太过于深究,因为操作系统会帮助我们调度。换句话说,我们一边打印word文档,一边听歌,一边写字

    虽然我们感觉是“同时”进行的,但是其实是CPU是在后台不停的帮助我们切换进程,只是CPU切换的速度太快了,让我们感觉我们是在“同时”做很多件事。

     

    (一)基本异步示例

    下面代码演示了一个基本上异步程序:(程序使用VS2022+.NET 7.0 开发的)

    (1)HandleFileAsync() 表示这是一个异步的方法,方法名称前有一个 await 关键字。

     作为一个约定,方法总是以Async结尾,这样,使用者看到这个方法就知道了这是一个异步方法,这仅仅是方法名称的一个约定,不加Async不影响使用。

    (2)在HandleFileAsync 方法里,模拟执行一些费时的操作。

    (3)在HandleFileAsync 执行期间,不会阻塞主线程,现在输入字符串 123 ,系统会显示出入的结果。

    (4)在异步方法执行完毕后,返回主线程,输出计数的结果。

    复制代码
    using System;
    using System.IO;
    using System.Threading.Tasks;
    
    class Program
    {
        public static void Main()
        {
            // Part 1: 开始处理大文件文件
            Task<int> task = HandleFileAsync();
    
            // 在文件处理前,把控制权交给控制台
            // 让用户输入一些文字
            Console.WriteLine("请耐心等待,系统正在处理文件," +" 但是此时,你可以输入一些字母,回车后显示");
    
            // 在文件处理时,同时读取你的输入
            string line = Console.ReadLine();
            Console.WriteLine("你刚刚输入的是: " + line);
    
            // Part 3: 等候处理结果
            //  显示处理结果
            task.Wait();
            var x = task.Result;
            Console.WriteLine("计数: " + x);
    
            Console.WriteLine("程序运行完毕!");
            Console.ReadLine();
        }
    
        static async Task<int> HandleFileAsync()
        {
            string file = @"C:\qmx\token.txt";
    
            // Part 2: 下面开始处理大文件
    
            Console.WriteLine("文件处理开始");
            int count = 0;
    
            // 读取文件
            using (StreamReader reader = new StreamReader(file))
            {
                string v = await reader.ReadToEndAsync();
    
                // 处理数据
                count += v.Length;
    
               
                // 这里是模拟代码,并没有实际的意义, 让程序执行1000万次,
                // 纯粹是模拟这是一个耗时的操作
                for (int i = 0; i < 1000000; i++)
                {
                    int x = v.GetHashCode();
                    if (x == 0)
                    {
                        count--;
                    }
                }
            }
            Console.WriteLine("文件处理结束");
            return count;
        }
    } 
    复制代码

     

     下面显示的是运行结果

     

     当然上面后面的代码可以简写为  var x =await task.Result; 

     

     (二)线程阻塞(死锁)

     在上面方法里,必须小心的调用 Wait方法,因为处理不好,很容易发生任务阻塞。 Stephen Cleary 曾经给了一个典型的例子:见 

    https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

    想象一下,我们有一个winForm应用程序,里面有一个Button,在Button的点击事件里,我们调用 HttpClient 的 GetStringAsync 方法获取返回的JSON字符串,然后把字符串显示在文本框里。

    为此,我们编写了如下代码:

    复制代码
    // Button的点击事件
    public void Button1_Click(...)
    {
    //获取Web返回的字符串
      var jsonTask = GetJsonAsync(...);
    //把字符串显示在文本框里
      textBox1.Text = jsonTask.Result;
    }
    
    
    public static async Task GetJsonAsync(Uri uri)
    {
      // 调用 HttpClient 的 GetStringAsync 方法获取JSON
      using (var client = new HttpClient())
      {
        var jsonString = await client.GetStringAsync(uri);
        return JObject.Parse(jsonString);
      }
    }
    复制代码

    现在你运行上面的代码,当你点击按钮时,你会发现程序没有出现你所想要的结果:因为程序被卡死了,根本无法进行其他操作。
    除了终止应用程序,你别无选择。为什么会发生什么死锁现象呢? 

    为了让通俗解释死锁看下面一个例子:假设我们有一把蓝钥匙,可以打开一扇蓝色门;以及一把红钥匙,可以打开一扇红色门。两把钥匙被保存在一个皮箱里。同时我们定义六种行为:获取蓝钥匙,打开蓝色门,归还蓝钥匙,获取红钥匙,打开红色门,归还红钥匙。如下图:你可以把6个行为理解为函数里6个方法 (以下内容改写自知乎

     

    如果是同步编程,方法一个个调用,没有问题

    但是,当异步调动时,每个方法顺序就不那么确定了,就可能出现如下这个情况

     

    可以看到,当两个线程都运行到第三步的时候,线程A在等线程B归还红钥匙,线程B在等线程A归还蓝钥匙,因而两个线程都永远卡在那里无法前进。
    这就是形成了死锁。

    理解了上面的死锁,回头再来看为什么winForm里产生了死锁,主线程调用异步方法返回的结果,被告知方法未完成,因此主线程在等待方法完成。

    当异步方法完成后,把自己状态告知主线程已经Compled时,但是主线程一直在繁忙状态,他在等待任务完成,因此,发生了死锁。

    这告诉我们在异步编程时,要特别需要注意死锁的问题。作为一个简单的解决方法:只要加一个await 异步就可以了

    public async void Button1_Click(...)
    {
      var json = await GetJsonAsync(...);
      textBox1.Text = json;
    }

    这也就是大家常说“一路异到底”。(不要在同步方法里调用异步方法,要异步调用异步,一路异到底)

     

    (三)ContinueWith

     

    在现实世界里,经常会发生在一个方法完成之后,在进行下一个方法的调用,
    例如,在Button 事件里 (1)异步从网络获取HTML源代码。 (2)把源代码写入 C:\File.txt 里

    这就需要第二步骤需要在第一步完成之后运行,此时需要用到ContinueWith 方法。

    下面的代码简单演示了 ContinueWith  (其实,ContinueWith 这个方法的名字就已经很好的解释了他的作用)

    复制代码
    using System;
    using System.Threading.Tasks;
    
    class Program
    {
        static void Main()
        {
            //调用10次异步方法
            for (int i = 0; i < 10; i++)
            {
                Run2Methods(i);
            }
            //所有调用都是异步
            Console.ReadLine();
        }
    
        static async void Run2Methods(int count)
        {
            // 在调动完后,调用 ContinueWith 继续操作 
            int result = await Task.Run(() => GetSum(count))
                .ContinueWith(task => MultiplyNegative1(task));
            Console.WriteLine("Run2Methods 结果: " + result);
        }
    
        static int GetSum(int count)
        {
            //这里模拟一些额外操作
            int sum = 0;
            for (int z = 0; z < count; z++)
            {
                sum += (int)Math.Pow(z, 2);
            }
            return sum;
        }
    
        static int MultiplyNegative1(Task<int> task)
        {
            //  这里模拟对数字取其相反数
            return task.Result * -1;
        }
    }
    复制代码

    下面显示了运行结果

     

    上面简单的介绍了异步编程。

  • 相关阅读:
    作业练习2:类与数据结构
    JDBC学习笔记(2)事务
    HTML5学习系列之主结构
    ubuntu 20.04 + 3090Ti 深度学习装机 (Nvidia-显卡驱动-515.57 + CUDA-11.7 + cuDNN-8.4.1)
    数据结构第5章课后习题答案
    【CBAM 解读】混合注意力机制:Convolutional Block Attention Module
    IP地址和MAC地址
    Springboot中集成mongodb,mysql(密码从密码服务中获取并且动态更新)
    axios.js get 请求 生成参数的方法,兼容golang gin
    【Linux】常用命令总结(简略版)
  • 原文地址:https://www.cnblogs.com/mqingqing123/p/17496041.html