前言
拖了很长时间,我决定还是发下吧
这次就没啥好说的,主要就讲一个地方——什么是异步,同时会顺带同步是什么以及C#中异步的实现
事先声明,这只能算C#异步入门的相关文章,若需要详细了解请到该微软官方文档上看,链接在此:异步编程 - C# | Microsoft Learn
异步和同步
在正式讲异步之前,我觉得有必要先讲讲同步是什么,这样,我才能方便讲什么叫做异步
同步定义
简单来说,同步的核心特点就是:“调用方必须等待任务完成,才能继续执行后续操作”,如果当前任务没完成,之后的任务就必须等待
举个例子,一个线性闯关游戏,你必须要先把A关打了,才能去接着打B关,B关打了才能去打C关等等,而打的时候你必须专心完成这一关,而其他的事情你都不能做
异步定义
异步则不同,异步的核心特点便是:当前任务在等待结果时,可以先去处理其他任务。
就相当于你做饭的时候,可以同时挂着一个挂机游戏,做饭需要一定的时间,而在做饭的时候,挂机游戏仍然在后台运行,等到游戏完成某个任务之后,才告诉你去领奖励
很显然,这种方法并不是按顺序进行,而是任务在等待 I/O 时让出线程控制权
当然,这并不是什么多线程,很多异步任务只是在线程空闲时调度执行的
两者的区别
既然讲完了什么是同步,什么是异步,那区别就很明显了,若还是看不出来,那我再换个说法:
同步:同步就是所有事情都要你一个人来依次完成
异步:异步就是部分任务可以交给其他人或系统处理,而你可以作为一个领头人,只需要在结果完成时再继续处理
当然了,异步并不一定总是比同步更快
例如前面提到的闯关游戏,如果游戏机制规定必须按顺序通关,那么就算是使用异步,也没办法减少时间
因此,是否使用异步,主要取决于:
任务之间是否存在等待时间
任务之间是否存在依赖的关系
根据这些就可以判断是否需要使用异步
至此,什么是异步/同步的基本内容讲完了,接下来就讲讲C#的异步了
C#异步
题外话
大部分人入门所写的代码,其实默认就是同步的代码
所以我在这里就不多扯跟同步相关的代码了,直接进入异步这部分
C#异步编程概述
这里就不多废话了,前文定义也讲过一些东西,我直接引用微软官方的文档吧(原链接:异步编程场景 - C# | Microsoft Learn):
在 C# 代码中实现异步编程时,编译器会将程序转换为状态机。 此构造跟踪代码中的各种操作和状态,例如在代码到达 await 表达式时暂停执行,并在后台任务完成时恢复执行。
就计算机科学理论而言,异步编程是 异步编程模型的实现。
简单来说,就是C#的async / await本质上是拿状态机实现的
至于为什么不直接手写异步逻辑,是因为C#的异步本身是通过TAP的方式来实现的,而async / await只是 TAP 的一种语法封装
所以,这种玩意非特必要才学呗,如果对该方法有兴趣的就可以看看这个:基于任务的异步模式(TAP):简介和概述 - .NET | Microsoft Learn
对于微软官方的态度似乎也是不建议手写TAP来实现异步的,所以,在这里,我就主要讲讲async / await 版的异步
核心
核心部分
在C#的异步编程中,最为基本的便是这四个:
Task表异步任务Task<T>表带返回值的异步任务async声明方法的await是等待异步任务完成的
当然,这里插一嘴,async不是必须的,毕竟await要的是可以返回Task类型的,但也很重要
同时在C#中,异步有俩种异步绑定方式,一类是I/O 绑定,另一类是CPU 绑定,这俩者的最直观的区别仅在于是否使用了Task.Run 上,本质上最重要的区别还是用没用CPU,以及CPU绑定要Task.Run
但在性能消耗上,I/O 绑定很明显比CPU 绑定少很多,但分别的应用场景则不同:
I/O 绑定就是所谓异步方法的调用顺序
而CPU 绑定这个玩意,顾名思义,就是用在CPU上,主要解决的就是CPU 响应能力方面的问题
当然,无论怎么绑定,最为核心的地方在于你需要使用async 这个关键词,你才能去使用C#的异步编程,当然,记得引用using System;和using System.Threading.Tasks;(Task所需)
async和await的本质
async
前文提到过async 是什么玩意,这里就详细说明下:
async是声明异步方法的,这玩意有点类似于java版的注解,只不过这个玩意是直接调用并立即返回 Task的
讲完async,接下来简单讲讲await
await
前文所述,await 是等待异步任务完成的,但你可能会理解成等待之类,
但实际上真等待那是同步才会干的,await 并不会阻塞线程,而是会将方法拆分为两部分:
执行 await 之前
任务完成后的 continuation
当任务完成时,方法会从 await 后继续执行
怎么去使用
在微软官方中,对于异步的使用讲的很清楚了,主要是这几点(链接:异步编程场景 - C# | Microsoft Learn):
可以对 I/O 绑定和 CPU 绑定代码使用异步代码,但实现不同。
异步代码使用
Task<T>和Task对象作为构造来为在后台运行的工作建模。关键字
async将方法声明为异步方法,这样就可以在方法正文中使用await关键字。应用
await关键字时,代码将挂起调用的方法,并将控制权交还给其调用者,直到任务完成。只能在异步方法中使用
await表达式。
注意事项
微软官方其实在这方面讲的很详细了,但我还是决定再复述一遍:
一、async方法内要加await
很显然,正如前文所述async方法本身编译后就是一个状态机,当然你也可以无视如下图的警告直接使用该方法,不是说不行,vs弹出来的也不是错误而是警告,但你不加await显然是浪费了一个状态机,若无必须不要闲着没事去搞这种(这里就不贴原文了,意思一样的)

二、仅从事件处理程序返回“async void”
下面是微软原话(链接:异步编程场景 - C# | Microsoft Learn):
事件处理程序必须声明
void返回类型,不能像其他方法一样使用或返回Task对象Task<T>。 编写异步事件处理程序时,需要对处理程序的返回方法使用async修饰符void。 返回方法的其他实现async void不遵循 TAP 模型,并且可能会带来挑战:
async void方法中引发的异常无法在该方法外部被捕获
async void方法难以测试
async void方法在调用方未期望其为异步时可能会导致负面效果
总的来说,在微软角度来说,主要就是代码规范的问题,毕竟事件处理程序本身是一定要用async void的,但除此之外就不能滥用之类,async是什么我不再补充,只用知道后面的void返回不了东西,就导致不能使用await之类,如果乱用那就会降低可读性什么的(毕竟这玩意不符合TAP规范)
三、LINQ中谨慎使用lambda
好吧,我先提俩嘴什么是LINQ和lambda ,如果不知道的,可以去看折叠部分
LINQ和lambda
lambda你可以粗暴的认为就是个加速开发的神器,他能为你节省大量开发时间
而LINQ就是语言集成查询的缩写,主要的作用跟mssql的查询功能差不多,完全可以理解为C#内置版mssql的查询功能
我知道肯定会有人说:“啊真理,这个不是废话吗?”我说,对于这一块微软在官方文档也表明了,说:使用LINQ上使用异步lambda会出现一些bug
这里是原文(链接:异步编程场景 - C# | Microsoft Learn):
在 LINQ 表达式中实现异步 lambda 时,请务必小心。 LINQ 中的 Lambda 表达式使用延迟执行,这意味着代码可以在意外时间执行。 如果代码编写不正确,在这种情况下引入阻止任务很容易导致死锁。 此外,异步代码的嵌套也使得难以推理代码的执行。 Async 和 LINQ 非常强大,但这些技术应尽可能谨慎且清晰地一起使用。
就先讲这么多,若接着讲下去那我就是微软文档的“搬运工”而不是给你入门的博文,在这里贴上链接:异步编程场景 - C# | Microsoft Learn
示例
接下来就是最经典的示例部分了,在这里,我就只讲I/O 绑定这一块,仅用来直观演示:
这里就是烧水喝水的一个问题了,当然这里不考虑说需要时间,以及喝水的人能否承受刚烧开的开水等问题
主要是这样的流程:
等待烧水(2秒)→学习(3秒)→喝水(0.5秒)
若同步,则需要5.5秒的时间,这显然太花时间了,于是就用到异步了,异步会将学习的部分串在等水、喝水,以节省时间,以下便是异步的代码:
using System;
using System.Diagnostics;
class Program
{
static async Task Main(string[] args)
{
var sw = Stopwatch.StartNew(); //你只用知道这是计时的就行
Console.WriteLine("Boiling water..."); //开始并等待烧水完成
Task walkingTask = Waiting(); //开始等待开水,但不等待它完成
Task studyTask = Study(); //开始学习,但不等待它完成
await walkingTask; //现在等待走路完成
await Drink(); //等待喝水完成
await studyTask; //现在等待学习完成
sw.Stop();
Console.WriteLine($"消耗时间: {sw.ElapsedMilliseconds} ms");
}
//异步方法示例
//等开水
static async Task Waiting()
{
Console.WriteLine("Waiting...");
await Task.Delay(2000); //模拟一个耗时操作,比如走路需要2秒
/*这个就等同于同步方法的Thread.Sleep(2000);,也就是等五秒后执行
但是它不会阻塞主线程,
而是让出控制权,允许其他代码继续执行*/
Console.WriteLine("Finished waiting.");
}
//学习
static async Task Study()
{
Console.WriteLine("Studying...");
await Task.Delay(3000);
Console.WriteLine("Finished Studying");
}
//喝水
static async Task Drink()
{
Console.WriteLine("Drinking...");
await Task.Delay(500);
Console.WriteLine("Drinking water finished");
}
}结果:
Boiling water...
Waiting...
Studying...
Finished waiting.
Drinking...
Drinking water finished
Finished Studying
消耗时间: 3028 ms
就这样吧