zoukankan      html  css  js  c++  java
  • C#异步编程由浅入深(二)Async/Await的作用.

      考虑到直接讲实现一个类Task库思维有点跳跃,所以本节主要讲解Async/Await的本质作用(解决了什么问题),以及Async/Await的工作原理。实现一个类Task的库则放在后面讲。首先回顾一下上篇博客的场景。

     class Program
        {
    
            public static string GetMessage()
            {
                return Console.ReadLine();
            }
    
            public static  string TranslateMessage(string msg)
            {
                return msg;
            }
    
            public static  void DispatherMessage(string msg)
            {
                switch (msg)
                {
                    case "MOUSE_MOVE":
                        {
                            OnMOUSE_MOVE(msg);
                            break;
                        }
                    case "MOUSE_DOWN":
                        {
                            OnMouse_DOWN(msg);
                            break;
                        }
                    default:
                        break;
                }
            }
    
            public static void OnMOUSE_MOVE(string msg)
            {
                Console.WriteLine("开始绘制鼠标形状");
            }
    
    
            public static int Http()
            {
                Thread.Sleep(1000);//模拟网络IO延时
                return 1;
            }
            public static void HttpAsync(Action<int> action,Action error)
            {
                //这里我们用另一个线程来实现异步IO,由于Http方法内部是通过Sleep来模拟网络IO延时的,这里也只能通过另一个线程来实现异步IO
                //但记住,多线程是实现异步IO的一个手段而已,它不是必须的,后面会讲到如何通过一个线程来实现异步IO。
                Thread thread = new Thread(() => 
                {
                    try
                    {
                        int res = Http();
                        action(res);
                    }
                    catch
                    {
                        error();
                    }
          
                });
    
                thread.Start();
            }
    
            public static Task<int> HttpAsync()
            {
                return Task.Run(() => 
                {
                    return Http();
                });
            }
    
    
            public static void OnMouse_DOWN(string msg)
            {
                HttpAsync()
                    .ContinueWith(t => 
                    {
                        if(t.Status == TaskStatus.Faulted)
                        {
    
                        }else if(t.Status == TaskStatus.RanToCompletion)
                        {
                            Console.WriteLine(1);
                            //做一些工作
                        }
                    })
                    .ContinueWith(t => 
                    {
                        if (t.Status == TaskStatus.Faulted)
                        {
    
                        }
                        else if (t.Status == TaskStatus.RanToCompletion)
                        {
                            Console.WriteLine(2);
                            //做一些工作
                        }
                    })
                    .ContinueWith(t => 
                    {
                        if (t.Status == TaskStatus.Faulted)
                        {
    
                        }
                        else if (t.Status == TaskStatus.RanToCompletion)
                        {
                            Console.WriteLine(3);
                            //做一些工作
                        }
                    });
            }
    
            static void Main(string[] args)
            {
                while (true)
                {
                    string msg = GetMessage();
                    if (msg == "quit") return;
                    string m = TranslateMessage(msg);
                    DispatherMessage(m);
                }
            }
        }
    
    

      在OnMouse_DOWN这个处理函数中,我们使用Task的ContinueWith函数进行链式操作,解决了回调地狱问题,但是总感觉有点那么不爽,我们假想有个关键字await它能实现以下作用:首先await必须是Task类型,必须是Task类型的(其实不是必要条件,后面会讲到)原因是保证必须有ContinueWith这个函数,如果Task没有返回值,则把await后面的代码放到Task中的ContinueWith函数体内,如果有返回值,则把Await后的结果转化为访问Task.Result属性,文字说的可能不明白,看下示例代码

    //无返回值转换前
    public async void Example()
    {
        Task t = Task.Run(() =>
        {
            Thread.Sleep(1000);
        });
        await t;
        //做一些工作
    }
    //无返回值转换后
    public void Example()
    {
        Task t = Task.Run(() =>
        {
            Thread.Sleep(1000);
        });
        t.ContinueWith(task => 
        {
            //做一些工作
        });
    }
    
    //有返回值转换前
    public async void Example()
    {
        Task<int> t = Task.Run<int>(() =>
        {
            Thread.Sleep(1000);
            return 1;
        });
        int res = await t;
        //使用res做一些工作
    }
    //有返回值转换后
    public void Example()
    {
        Task<int> t = Task.Run<int>(() =>
        {
            Thread.Sleep(1000);
            return 1;
        });
        t.ContinueWith(task => 
        {
            //使用task.Result做一些工作
        });
    }
    

      看起来不错,但至少有以下问题,如下:

    • 该种转换方法不能很好的转换Try/Catch结构
    • 在循环结构中使用await不好转换
    • 该实现与Task类型紧密联系

      一二点是我自己认为的,但第三点是可以从扩展async/await这点被证明的。但无论怎样,async/await只是对方法按照一定的规则进行了变换而已,它并没有什么特别之处,具体来讲,就是把Await后面要执行的代码放到一个类似ContinueWith的函数中,在C#中,它是以状态机的形式表现的,每个状态都对应一部分代码,状态机有一个MoveNext()方法,MoveNext()根据不同的状态执行不同的代码,然后每个状态部分对应的代码都会设置下一个状态字段,然后把自身的MoveNext()方法放到类似ContinueWith()的函数中去执行,整个状态机由回调函数推动。我们尝试手动转换以下async/await方法。

    public static Task WorkAsync()
    {
        return Task.Run(() => 
        {
            Thread.Sleep(1000);
            Console.WriteLine("Done!");
        });
    }
    public static async void Test()
    {
        Console.WriteLine("步骤1");
        await WorkAsync();
        Console.WriteLine("步骤2");
        await WorkAsync();
        Console.WriteLine("步骤3");
    }
    

      手动写一个简单的状态机类

    public class TestAsyncStateMachine
        {
            public int _state = 0;
            public void Start() => MoveNext();
            public void MoveNext()
            {
                switch(_state)
                {
                    case 0:
                        {
                            goto Step0;
                        }
                    case 1:
                        {
                            goto Step1;
                        }
                    default:
                        {
                            Console.WriteLine("步骤3");
                            return;
                        }
                }
    
            Step0:
                {
                    Console.WriteLine("步骤1");
                    _state = 1;
                    WorkAsync().ContinueWith(t => this.MoveNext());
                    return;
                }
            Step1:
                {
                    _state = -1;
                    Console.WriteLine("步骤2");
                    WorkAsync().ContinueWith(t => this.MoveNext());
                    return;
                }
    
            }
        }
    

      而Test()方法则变成了这样

    public static void Test()
    {
        new TestAsyncStateMachine().Start();
    }
    

      注意Test()方法返回的是void,这意味这调用方将不能await Test()。如果返回Task,这个状态机类是不能正确处理的,如果要正确处理,那么状态机在Start()启动后,必须返回一个Task,而这个Task在整个状态机流转完毕后要变成完成状态,以便调用方在该Task上调用的ContinueWith得以继续执行,而就Task这个类而言,它是没有提供这种方法(内部有,但没有对外暴露)来主动控制Task的状态的,这个与JS中的Promise不同,JS里面用Reslove函数来主动控制Promise的状态,并导致在该Promise上面的Then链式调用得以继续完成,而在C#里面怎么做呢?既然使用了状态机来实现async/await,那么在转换一个返回Task的函数时肯定会遇到,怎么处理?后面讲。
      首先解决一下与Task类型紧密联系这个问题。
      从状态机中可以看到,主要使用到了Task中的ContinueWith这个函数,它的语义是在任务完成后,执行回调函数,通过回调函数拿到结果,这个编程风格也叫做CPS(Continuation-Passing-Style, 续体传递风格),那么我们能不能把这个函数给抽象出来呢?语言开发者当然想到了,它被抽象成了一个Awaiter因此编译器要求await的类型必须要有GetAwaiter方法,什么样的类型才是Awaiter呢?编译器规定主要实现了如下几个方法的类型就是Awaiter:

    • 必须继承INotifyCompletion接口,并实现其中的OnCompleted(Action continuation)方法
    • 必须包含IsCompleted属性
    • 必须包含GetResult()方法

      第一点好理解,第二点的作用是热路径优化,第三点以后讲。我们再改造一下我们手动写的状态机。

    public class TestAsyncStateMachine
    {
        public int _state = 0;
        public void Start() => MoveNext();
        public void MoveNext()
        {
            switch(_state)
            {
                case 0:
                    {
                        goto Step0;
                    }
                case 1:
                    {
                        goto Step1;
                    }
                default:
                    {
                        Console.WriteLine("步骤3");
                        return;
                    }
            }
    
        Step0:
            {
                Console.WriteLine("步骤1");
                _state = 1;
                TaskAwaiter taskAwaiter;
                taskAwaiter = WorkAsync().GetAwaiter();
                if (taskAwaiter.IsCompleted) goto Step1;
                taskAwaiter.OnCompleted(() => this.MoveNext());
                return;
            }
        Step1:
            {
                _state = -1;
                Console.WriteLine("步骤2");
                TaskAwaiter taskAwaiter;
                taskAwaiter = WorkAsync().GetAwaiter();
                if (taskAwaiter.IsCompleted) MoveNext();
                taskAwaiter.OnCompleted(() => this.MoveNext());
                return;
            }
    
        }
    }
    

      可以看到去掉了与Task中ContinueWith的耦合关系,并且如果任务已经完成,则可以直接执行下个任务,避免了无用的开销。
      因此我们可以总结一下async/await:

    • async/await只是表示这个方法需要编译器进行特殊处理,并不代表它本身一定是异步的。
    • Task类中的GetAwaiter主要是给编译器用的。

      第一点我们可以用以下例子来证明,有兴趣的朋友可以自己去验证以下,以便加深理解。

    //该类型包含GetAwaiter方法,且GetAwaiter()返回的类型包含三个必要条件
    public class MyAwaiter : INotifyCompletion
    {
        public void OnCompleted(Action continuation)
        {
            continuation();
        }
    
        public bool IsCompleted { get; }
        public void GetResult()
        {
        
        }
    
        public MyAwaiter GetAwaiter() => new MyAwaiter();
    }
    

      一个测试函数,注意必须返回void

    public static async void AwaiterTest()
    {
        await new MyAwaiter();
        Console.WriteLine("Done");
    }
    

      可以看到这是完全同步进行的。
      觉得有收获的不妨点个赞,有支持才有动力写出更好的文章。

    知其然,而后知其所以然。
  • 相关阅读:
    Idea的类中使用实体类(有@Data注解)的Get/Set方法报错
    Springboot前后端分离中,后端拦截器拦截后,前端没有对应的返回码可以判断
    Window NodeJs安装
    Linux(CENTOS7) NodeJs安装
    Linux(CENTOS7) YUM方式安装mysql5.7
    根据M3U8地址下载视频
    Mysql时间范围分区(RANGE COLUMNS方式)
    Window Mysql5.7免安装版配置
    Window Jdk配置(win7/win10都可以)
    .net core2.0 读取appsettings.json
  • 原文地址:https://www.cnblogs.com/hkfyf/p/14641844.html
Copyright © 2011-2022 走看看