本文我们从一个生活中的场景为例开始 ,探究 C# 的 async / await

B站同名账号KrnlsYs有本文的视频版讲解,还有C#零基础4小时速通入门教程,读者可按需观看

https://b23.tv/GxdFluC

一、从生活中的场景理解同步和异步

先来设想一个场景:我饿了,要吃午饭,于是点了一份外卖。并且我还打算看视频学习C#

同步思维

  1. 点外卖

  2. 站在门口,什么也不干,一直等

  3. 半个小时后,外卖到了

  4. 开始吃饭,吃完

  5. 吃完之后,才开始学习 C#

这个流程是正确的,逻辑也很清晰:一步一步来,前一步不完成,后一步就不开始

但实际上,正常人点了外卖之后不会干等着,而是去做其他的事情

异步思维

更合理的方式是:

  1. 点外卖

  2. 在等外卖的过程中,开始学习 C#

  3. 外卖到了,边吃边继续看

  4. 吃完,学习也差不多结束了

做了同样的事,总耗时明显减少了

二、引入async和await

先来看一段完全同步的代码:

namespace KrnlsYs
{
	class Program
	{
		static void Show()
		{
			Console.WriteLine(2);

			Thread.Sleep(3000);

			Console.WriteLine(3);
		}
		static void Main(string[] args)
		{
			Console.WriteLine(1);

			Show();

			Console.WriteLine(4);

			Console.ReadLine();
		}
	}
}

这段代码非常简单,先输出1 2 三秒后输出 3 4 

Thread.Sleep 是同步(阻塞)的等待。不理解没关系,下文会和异步(不阻塞)对比

现在把Show方法改成异步版本:

static async void ShowAsync()
{
	Console.WriteLine(2);

	await Task.Delay(3000);

	Console.WriteLine(3);
}

注意:

  • 将Thread.Sleep修改为 await Task.Delay(3000);
  • 方法使用async关键字修饰,并且方法名称加上Async(异步方法的命名约定)
  • 绝大部分情况下不应该用void作为异步方法的返回值,如果不需要返回值应该使用 Task,需要额外返回值使用 Task<TResult>,本文暂不讨论

运行后你会看到:1 2 4 三秒后输出 3

三、await 到底做了什么

真正的关键,就在:await Task.Delay(3000);

await 的行为可以分成几步理解:

  1. 先判断:后面的这个 "东西" 是不是已经完成了

  2. 如果已经完成:像同步代码一样,继续往下执行

  3. 如果没有完成(需要等待)

    • 暂停当前方法

    • 把控制权还给调用方

    • 当前线程不阻塞,可以去干别的事

Task.Delay(3000) 真的完成之后:

  • 运行时会“通知”回来

  • 再继续执行 await 后面的代码

所以现在很好解释刚才的输出:

  • Main 输出 1

  • 调用 ShowAsync,输出 2

  • 遇到 await,方法暂停,直接返回

  • Main 继续执行,输出 4

  • 3 秒后,ShowAsync 恢复执行,输出 3

所以最终顺序是:1243


四、一个“点外卖”的完整异步示例

我们把最开始的点外卖的例子用代码实现

using System.Diagnostics;

namespace KrnlsYs
{
	class Program
	{
		static void OrderDelivery()
		{
			Console.WriteLine("Ordering delivery");
		}
		static void WaitForDelivery()
		{
			Console.WriteLine("Waiting...");
			Thread.Sleep(5000);

			Console.WriteLine("Delivery arrived");
		}
		static void Eat()
		{
			Console.WriteLine("Eating...");
			Thread.Sleep(5000);

			Console.WriteLine("Finished eating");
		}
		static void LearnCSharp()
		{
			Console.WriteLine("Learning...");
			Thread.Sleep(10000);
			Console.WriteLine("Finished learning C#");
		}

		static void Main(string[] args)
		{
			var sw = Stopwatch.StartNew();

			OrderDelivery();
			WaitForDelivery();
			Eat();
			LearnCSharp();

			sw.Stop();
			Console.WriteLine(sw.Elapsed.TotalSeconds);
		}
	}
}

同步版本:总耗时 20 秒 = 等外卖 5 秒 + 吃饭 5 秒 + 学习 C# 10 秒

异步版本:并行等待

using System.Diagnostics;

namespace KrnlsYs
{
	class Program
	{
		static void OrderDelivery()
		{
			Console.WriteLine("Ordering delivery");
		}
		static async Task WaitForDeliveryAsync()
		{
			Console.WriteLine("Waiting...");
			await Task.Delay(5000);

			Console.WriteLine("Delivery arrived");
		}
		static async Task EatAsync()
		{
			Console.WriteLine("Eating...");
			await Task.Delay(5000);

			Console.WriteLine("Finished eating");
		}
		static async Task LearnCSharpAsync()
		{
			Console.WriteLine("Learning...");
			await Task.Delay(10000);
			Console.WriteLine("Finished learning C#");
		}

		static async Task Main(string[] args)
		{
			var sw = Stopwatch.StartNew();

			OrderDelivery();
			var waitingTask = WaitForDeliveryAsync();
			var learningTask = LearnCSharpAsync();

			await waitingTask;
			await EatAsync();

			await learningTask;

			sw.Stop();
			Console.WriteLine(sw.Elapsed.TotalSeconds);
		}
	}
}

等外卖的同时开始学习,外卖到后开始吃,吃完时,学习也刚好完成,总耗时10秒

五、一个非常重要的误区:await 等的是什么?

很多人会误以为:只有 async 方法才能被 await,这是错误的

例如如下代码 await Show(); 行错误,因为Show无法被await

namespace KrnlsYs
{
	class Program
	{
		static async void Show()
		{
		}
		static async Task Main(string[] args)
		{
			await Show();
		}
	}
}

正确的说法是:await 等待的是一个awaitable expression 可等待表达式,因此不论方法是不是async,只要返回的可等待的,那么就可以await

如下代码的Show没有被async修饰,但是可以await

namespace AsyncExample
{
	class Program
	{
		static Task Show() => new(() => { });
	
		static async Task Main(string[] args)
		{
			await Show();
		}
	}
}

可以简单理解为:

  1. async 的作用,只是为了让方法内部可以使用 await
  2. 真正的异步行为,发生在 await 上,而不是 async 上
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐