async/await Task.Delay 和Thread.Sleep的理解

相关学习资料:

第十七节:从状态机的角度async和await的实现原理(新) - Yaopengfei - 博客园 (cnblogs.com)

[基础知识]有限状态机_哔哩哔哩_bilibili

C# async await 原理:编译器如何将异步函数转换成状态机 | 码农网 (codercto.com)

await——调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续的代码,当前线程不会阻塞

从原理层面刨析

  1. async与await是语法糖,最终译成“状态机调用”。其他常用的语法糖var、using、lambda表达式等。

  2. async关键字标记的方法会被C#编译器编译成一个状态机。

  3. await是关键字是为了实现状态机中的一个状态。

    会根据方法内的await调用切分成多个状态,每当有一个await,就会生成一个对应的状态。

  4. 状态机实现了AsyncStateMachine接口,里面有MoveNext 和 SetStateMachine方法处理相应业务.

    状态机实现接口

    MoveNext——定义各个状态之间转换的方法

    1. 在await执行时调用一次,在await操作结束时继续调用

从实践方向刨析

  1. async会将方法的返回结果包装成Task,所以它不是必需的

            //使用async,实际是将Result包装成Task<Result>
            //此处就是对结果通过await拆包,再通过async包装成Task<Result>返回,当前场景下没有必要,可直接简写成下面的场景
            public static async Task AsyncNoReturn()
            {
               await Task.Delay(500);
            }
            //不使用async的写法,因为类型已经是Task了不需要通过async关键字包装
            public static  Task AsyncNoReturn1()
            {
                return Task.Delay(500);
            }
    

    总结:如果一个异步方法只是对别的异步方法调用的转发,并没有太多复杂的逻辑(比如等待的结果,再调用B:把调用的返回值拿到内部做一些处理再返回),那么就可以去掉async关键字。返回值为Task的方法不一定都要标注async,标注async只是让我们可以更方便的await而已

    1. 避免对返回结果Task的“拆包后再次包装”
    2. 避免在底层创建状态机,性能更优

    正面Demo:

            public static Task<string> DownloadFromFile(int num)
            {
                if (num == 1)
                {
                   return File.ReadAllTextAsync(@"D:\1.txt");
                }
                else if (num == 2)
                {
                    return File.ReadAllTextAsync(@"D:\2.txt");
                }
                else
                {
                    throw new ArgumentException("Invalid Number");
                }
            }
    

    如果没有在声明Task的时候前面没有加await,则主线程不会异步等待Task完成,主线程会继续往下执行,与Task并发执行

    Task.Delay() 和 Thread.Sleep() 区别

Thread.Sleep() ——会让当前线程休眠,形成阻塞,当前休眠结束后继续往下执行

await Task.Delay()——当前线程返回线程池,从线程池拿另外一个空闲线程线程做定时任务,等待任务结束后,从线程池拿到一个空闲线程,继续往下执行。

            await Task.Delay(200);
            //可理解等效成如下代码
            await Task.Run(() =>
            {
                Thread.Sleep(200);
            });

区别:

  1. Thread.Sleep会阻塞当前线程,但是从线程池另取线程;Task.Delay不会阻塞当前线程,但是会新取一个线程

  2. Thread.Sleep不可取消,Task.Delay可取消

            public static Task Test_Delay()
            {
                //创建一个5秒的异步等待
                Task delay1 = Task.Delay(TimeSpan.FromSeconds(5));
                return delay1;
            }
    

    在Main方法里测试

    static void Main(string[] args)
    {
        //Task在声明处就开始执行
    	var test = Test_Delay();
        //主线程等待2秒钟
    	Thread.Sleep(TimeSpan.FromSeconds(2));
    	Stopwatch sw = new Stopwatch();
    	sw.Start();
        //当前线程等待test指向的Task执行结束
    	test.Wait();
    	sw.Stop();
    	TimeSpan ts = sw.Elapsed;
    	Console.WriteLine($"Task.Delay执行结束,耗时:{ts.TotalMilliseconds}");
    	Console.ReadKey();
    }
    

    结果打印:

    Task.Delay执行结束,耗时:3004.527
    

    上面显示大概监听为3秒。这个表示了当前主线程在运行的时候,Task.Delay也在运行,这个只有在不同线程才可以实现。