2020年05月12日

C#异步代码async/await解析,用await等待和同步阻塞有什么区别

作者 非鱼

先看这样一段常规代码:(假设两个耗时的方法为文件或数据库或网络请求操作)

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            var result = CallMethod();
            Console.WriteLine($"After method, Total: {result}");
            Console.ReadKey();
        }

        public static int CallMethod()
        {
            var a = Method1();
            var b = Method2();
            Console.WriteLine($"Result:{a}, {b}");
            return a + b;
        }

        public static int Method1()
        {
            Console.WriteLine("Method1 start.");
            Thread.Sleep(10000);
            Console.WriteLine("Method1 end in 10s.");
            return 1;
        }
        
        public static int Method2()
        {
            Console.WriteLine("Method2 start.");
            Thread.Sleep(5000);
            Console.WriteLine("Method2 end in 5s.");
            return 2;
        }
    }

执行结果:

Hello World!
Method1 start.
Method1 end in 10s.
Method2 start.
Method2 end in 5s.
Result:1, 2
After method, Total: 3

两个子方法运行分别需要10秒和5秒,所以整个方法总共需要15秒时间。

接下来我们把两个子方法转换成异步方法:

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            var res = Task.Run(CallMethod).GetAwaiter().GetResult();
            Console.WriteLine($"After method, Total: {res}");
            Console.ReadKey();
        }

        public static async Task<int> CallMethod()
        {
            var a = await Method1();
            var b = await Method2();
            Console.WriteLine($"Result:{a}, {b}");
            return a + b;
        }

        public static async Task<int> Method1()
        {
            var a = await Task.Run(() =>
            {
                Console.WriteLine("Method1 start.");
                Thread.Sleep(10000);
                Console.WriteLine("Method1 end in 10s.");
                return 1;
            });
            return a;
        }

        public static async Task<int> Method2()
        {
            var a = await Task.Run(() =>
            {
                Console.WriteLine("Method2 start.");
                Thread.Sleep(5000);
                Console.WriteLine("Method2 end in 5s.");
                return 2;
            });
            return a;
        }
    }

因为是在Console程序里,Main方法本身不允许加async参数,所以不能直接使用await,要使用GetResult方式获取异步结果。程序执行结果:

 Hello World!
 Method1 start.
 Method1 end in 10s.
 Method2 start.
 Method2 end in 5s.
 Result:1, 2
 After method, Total: 3

嗯?执行结果和顺序一模一样,执行时间也一样,还是15秒。await一个异步方法,和执行一个同步方法,效果是完全一样的吗?

我们来修改一下CallMethod方法:

        public static async Task<int> CallMethod()
        {
            var a = Method1();
            var b = Method2();
            var a1 = await a;
            var b1 = await b;
            Console.WriteLine($"Result:{a1}, {b1}");
            return a1 + b1;
        }

再执行一下,程序执行结果:

Hello World!
Method1 start.
Method2 start.
Method2 end in 5s.
Method1 end in 10s.
Result:1, 2
After method, Total: 3

OK,两个方法同时启动并行,时间短的先返回,时间长的后返回,但两个都返回以后,下面的代码才执行并返回。总执行时间变成了10秒。

唯一的区别是,把调用异步方法,和等待异步方法的结果,分成了两行来写。

这种情况下,如果是WebApi里面的某个接口,需要调用数据库执行5个不同的SQL语句(或者远程HTTP接口),将返回结果拼接起来返回给前端。如果每条SQL语句执行需要3秒(真是太慢了),那这个接口在顺序执行的情况下需要15秒才能返回,而在异步并行的情况下,总共只需要3秒。(前提是SQL数据库CPU没有受限)

接下来,假如CallMethod只需要执行几个异步任务,比如执行几个远程请求,再根据请求结果保存到数据库,并不需要返回结果,也就是void方法。把Main方法中调用的地方改成:

        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            Task.Run(CallMethod).Wait();
            Console.WriteLine($"After method.");
            Console.ReadKey();
        }

再运行一次:

Hello World!
Method1 start.
Method2 start.
After method.
Method2 end in 5s.
Method1 end in 10s.
Result:1, 2

结果虽然我们在Main方法里要求它等待CallMethod的执行,但是因为它是void方法,程序认为等待它的结果没有意义,然后就跳过了等待,继续执行了后面的代码。然后另外两个方法在后台默默的完成。如果是在一个WebApi的接口里,那结果就是接口立即返回了。