1、异步编程

异步编程是一项关键技术,可以直接处理多个核心上的阻塞 I/O 和并发操作。 通过 C#、Visual Basic 和 F# 中易于使用的语言级异步编程模型,.NET 可为应用和服务提供使其变得可响应且富有弹性。

上面是关于异步编程的解释,我们日常编程过程或多或少的会使用到异步编程,为什么要试用异步编程?因为用程序处理过程中使用文件和网络 I/O,比如处理文件的读取写入磁盘,网络请求接口API,默认情况下 I/O API 一般会阻塞。

这样的结果是导致我们的用户界面卡住体验差,有些服务器的硬件利用率低,服务处理能力请求响应慢等问题。基于任务的异步 API 和语言级异步编程模型改变了这种模型,只需了解几个新概念就可默认进行异步执行。

现在普遍使用的异步编程模式是TAP模式,也就是C# 提供的 async 和 await 关键词,实际上我们还有另外两种异步模式:基于事件的异步模式 (EAP),以及异步编程模型 (APM)

APM 是基于 IAsyncResult 接口提供的异步编程,例如像FileStream类的BeginRead,EndRead就是APM实现方式,提供一对开始结束方法用来启动和接受异步结果。使用委托的BeginInvoke和EndInvoke的方式来实现异步编程。

EAP 是在 .NET Framework 2.0 中引入的,比较多的体现在WinForm编程中,WinForm编程中很多控件处理事件都是基于事件模型,经常用到跨线程更新界面的时候就会使用到BeginInvoke和Invoke。事件模式算是对APM的一种补充,定义了一系列事件包括完成、进度、取消的事件让我们在异步调用的时候能注册响应的事件进行操作。

class Program
{
static void Main(string[] args)
{
Console.WriteLine(DateTime.Now + " start");
IAsyncResult result = BeginAPM();
//EndAPM(result);
Console.WriteLine(DateTime.Now + " end"); Console.ReadKey();
} delegate void DelegateAPM();
static DelegateAPM delegateAPM = new DelegateAPM(DelegateAPMFun); public static IAsyncResult BeginAPM()
{
return delegateAPM.BeginInvoke(null, null);
} public static void EndAPM(IAsyncResult result)
{
delegateAPM.EndInvoke(result);
}
public static void DelegateAPMFun()
{
Console.WriteLine("DelegateAPMFun...start");
Thread.Sleep(5000);
Console.WriteLine("DelegateAPMFun...end"); }
}

如上代码我使用委托实现异步调用,BeginAPM 方法使用 BeginInvoke 开始异步调用,然后 DelegateAPMFun 异步方法里面停5秒。看下下面的打印结果,是 main 方法里面的打印在前,异步方法里面的打印在后,说明该操作是异步的。

其中一行代码EndAPM(result)被注释了,调用了委托 EndInvoke 方法,该方法会阻塞程序直到异步调用完成,所以我们可以放到适当的位置用来获取执行结果,这类似于TAP模式的await 关键字,放开改行代码执行下。

以上两种方式已不推荐使用,编写理解起来比较晦涩,感兴趣的可以自行了解下,而且这种方式在.net 5里面已经不支持委托的异步调用了,所以如果要运行需要在.net framework框架下。

TAP 是在 .NET Framework 4 中引入的,是目前推荐的异步设计模式,也是我们本文讨论的重点方向,但是TAP并不一定是线程,他是一种任务,理解为工作的异步抽象,而非在线程之上的抽象。

2、async await

使用 async await 关键字可以很轻松的实现异步编程,我们子需要将方法加上 async 关键字,方法内的异步操作使用 await 等待异步操作完成后再执行后续操作。

class Program
{ static void Main(string[] args)
{
Console.WriteLine(DateTime.Now + " start");
AsyncAwaitTest();
Console.WriteLine(DateTime.Now + " end");
Console.ReadKey();
} public static async void AsyncAwaitTest()
{
Console.WriteLine("test start");
await Task.Delay(5000);
Console.WriteLine("test end");
}
}

AsyncAwaitTest 方法使用 async 关键字,使用await关键字等待5秒后打印"test end"。在 Main 方法里面调用 AsyncAwaitTest 方法。

使用 await 在任务完成前将控制让步于其调用方,可让应用程序和服务执行有用工作。 任务完成后代码无需依靠回调或事件便可继续执行。 语言和任务 API 集成会为你完成此操作。

使用await 的方法必须使用 async 关键字,如果我们 Main 方法里面想等待 AsyncAwaitTest 则 Main 方法需要加上 async 并返回 Task。

3、async await 原理

将上面 Main 方法不使用 await 调用的方式编译后使用ILSpy反编译dll,使用C# 4.0才能看到编译器为我们做了什么。因为4.0不支持 async await 所以会反编译到具体代码,4.0 以后的反编译后会直接显示 async await 语法。

通过反编译后可以看到在异步方法里面重新生成了一个泛型类 d__1 实现接口IAsyncStateMachine,然后调用Start方法,Start中进行了一些线程处理后调用 stateMachine.MoveNext() 即调用d__1实例化对象的MoveNext方法。

public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
if (stateMachine == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
}
Thread currentThread = Thread.CurrentThread;
Thread thread = currentThread;
ExecutionContext executionContext = currentThread._executionContext;
ExecutionContext executionContext2 = executionContext;
SynchronizationContext synchronizationContext = currentThread._synchronizationContext;
try
{
stateMachine.MoveNext();
}
finally
{
SynchronizationContext synchronizationContext2 = synchronizationContext;
Thread thread2 = thread;
if (synchronizationContext2 != thread2._synchronizationContext)
{
thread2._synchronizationContext = synchronizationContext2;
}
ExecutionContext executionContext3 = executionContext2;
ExecutionContext executionContext4 = thread2._executionContext;
if (executionContext3 != executionContext4)
{
ExecutionContext.RestoreChangedContextToThread(thread2, executionContext3, executionContext4);
}
}
}

我们再看编译器为生成的类 <AsyncAwaitTest>d__1

MoveNext方法将 AsyncAwaitTest 逻辑代码包含进去了,我们的源代码因为只有一个 await 操作,如果有多个 await 操作,那么MoveNext里面应该还会有多个分段逻辑,将不同段的MoveNext放入不同的状态分段块。

在该类中也有一个if判断,按照 1__state 状态参数,最开始调用的时候是-1,执行进来 num != 0 则执行我们的业务代码if里面的,这个时候会顺序执行业务代码,直到碰到 await 则执行如下代码

awaiter = Task.Delay(5000).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<> 1__state = 0); <> u__1 = awaiter; < AsyncAwaitTest > d__1 stateMachine = this; <> t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}

在该过程中 <> t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine) 将 await 句和状态机进行传递调用 AwaitUnsafeOnCompleted方法,该方法一直跟下去会找到线程池的操作。

// System.Threading.ThreadPool
internal static void UnsafeQueueUserWorkItemInternal(object callBack, bool preferLocal)
{
s_workQueue.Enqueue(callBack, !preferLocal);
}

程序将封装的任务放入线程池进行调用,这个时候异步方法就切换到了另一个线程,或者在原线程上执行(如果异步方法执行时间比较短可能就不会进行线程切换,这个主要看调度程序)。

执行完成 await 后状态 1__state 已经更改了为 0,程序会再次调用 MoveNext 进入 else 之后没有return和其它逻辑,则继续执行到结束。

可以看到这是一个状态控制的执行逻辑,是一种“状态机模式”的设计模式,对于 Main 方法调用 AsyncAwaitTest 逻辑此刻进入if,碰到await则进入线程调度执行,如果异步方法切换到其它线程调用,则方法 Main 继续执行,当状态机执行切换到另外一个状态后再次 MoveNext 直到执行完异步方法。

4、async 与 线程

有了上面的基础我们知道 async 与 await 通常是成对配合使用的,当我们的方法标记为异步的时候,里面的耗时操作就需要 await 进行标记等待完成后执行后续逻辑,调用该异步方法的调用者可以决定是否等待,如果不用 await 则调用者异步执行或者就在原线程上执行异步方法。

如果 async 关键字修改的方法不包含 await 表达式或语句,则该方法将同步执行,可选择性通过 Task.Run API 显式请求任务在独立线程上运行。

可以将 AsyncAwaitTest 方法改为显示线程运行:

public static async Task AsyncAwaitTest()
{
Console.WriteLine("test start");
await Task.Run(() =>
{
Thread.Sleep(5000);
});
Console.WriteLine("test end");
}

5、取消任务 CancellationToken

如果不想等待异步方法完成,可以通过 CancellationToken 取消该任务,CancellationToken 是一个struct,通常使用 CancellationTokenSource 来创建 CancellationToken,因为CancellationTokenSource 有一些列的[方法]用于我们取消任务而不用去操作CancellationToken 结构体。

CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;

然我改造下方法,将 CancellationToken 传递到异步方法,cts.CancelAfter(3000) 3秒钟后取消任务,我们监听CancellationToken 如果 IsCancellationRequested==true 则直接返回 。

static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
cts.CancelAfter(3000); Console.WriteLine(DateTime.Now + " start");
AsyncAwaitTest(ct);
Console.WriteLine(DateTime.Now + " end");
Console.ReadKey();
} public static async Task AsyncAwaitTest(CancellationToken ct)
{
Console.WriteLine("test start");
await Task.Delay(5000);
Console.WriteLine(DateTime.Now + " cancel");
if (ct.IsCancellationRequested) {
return;
}
//ct.ThrowIfCancellationRequested();
Console.WriteLine("test end");
}

因为我们是手动通过代码判断状态结束异步,所以即使在3秒后就已经结束了任务,但是await Task.Delay(5000) 任然会等待5秒执行完。还有一种方式就是我们不判断是否取消,直接调用ct.ThrowIfCancellationRequested() 给我们判断,这个方法如果,但是任然不能及时结束。这个时候我们还有另外一种处理方式,就是将CancellationToken 传递到 await 的异步API方法里,可能会立即结束,也可能不会,这个要取决异步实现。

public static async Task AsyncAwaitTest(CancellationToken ct)
{
Console.WriteLine("test start");
//传递CancellationToken 取消
await Task.Delay(5000,ct);
Console.WriteLine(DateTime.Now + " cancel"); //手动处理取消
//if (ct.IsCancellationRequested) {
// return;
//} //调用方法处理取消
//ct.ThrowIfCancellationRequested();
Console.WriteLine("test end");
}

6、注意项

在异步方法里面不要使用 Thread.Sleep 方法,有两种可能:

1、Sleep在 await 之前,则会直接阻塞调用方线程等待Sleep。

2、Sleep在 await 之后,但是 await 执行在调用方的线程上也会阻塞调用方线程。

所以我们应该使用 Task.Delay 用于等待操作。那为什么我上面的 Task.Run 里面使用了 Thread.Sleep呢,因为 Task.Run 是显示请求在独立线程上运行,所以我知道这里写不会阻塞调用方,上面我只是为了演示,所以不建议用。

.net 温故知新:【5】异步编程 async await的更多相关文章

  1. 温故知新,CSharp遇见异步编程(Async/Await),聊聊异步编程最佳做法

    什么是异步编程(Async/Await) Async/Await本质上是通过编译器实现的语法糖,它让我们能够轻松的写出简洁.易懂.易维护的异步代码. Async/Await是C# 5引入的关键字,用以 ...

  2. 异步编程Async/await关键字

    异步编程Async \await 关键字在各编程语言中的发展(出现)纪实. 时间 语言版本 2012.08.15 C#5.0(VS2012) 2015.09.13 Python 3.5 2016.03 ...

  3. 抓住异步编程async/await语法糖的牛鼻子: SynchronizationContext

    长话短说,本文带大家抓住异步编程async/await语法糖的牛鼻子: SynchronizationContext 引言 C#异步编程语法糖async/await,使开发者很容易就能编写异步代码. ...

  4. javascript异步编程 Async/await

    Async/await Async/await 在学习他之前应当补充一定的 promise 知识 它是一种与 promise 相配合的特殊语法,目前被认为是异步编程的终级解决方案 值得我们每一个人学习 ...

  5. [C#] 谈谈异步编程async await

    为什么需要异步,异步对可能起阻止作用的活动(例如,应用程序访问 Web 时)至关重要. 对 Web 资源的访问有时很慢或会延迟. 如果此类活动在同步过程中受阻,则整个应用程序必须等待. 在异步过程中, ...

  6. .net 异步编程async & await关键字的思考

    C# 5.0引入了两个关键字 async和await,这两个关键字在很大程度上帮助我们简化了异步编程的实现代码,而且TPL中的task与async和await有很大的关系 思考了一下异步编程中的asy ...

  7. 异步编程async/await

    什么是异步? 在异步程序中,程序代码不需要按照编写时的顺序严格执行,有时需要一在一个新的线程中运行一部分代码,有时无需创建新的 线程,但是为了更好的利用单个线程的能力,需要改变代码的执行顺序. 进程 ...

  8. c#异步编程async await

    可以代替协程了 但是需要.net4 版本 unity2017以上版本可以用了 再也可以不用蛋疼的没有返回值的协程了 //异步编程,和Task一起用 async void TestAsync(){ // ...

  9. .NetCore 异步编程 - async/await

    前言: 这段时间开始用.netcore做公司项目,发现前辈搭的框架通篇运用了异步编程方式,也就是async/await方式,作为一个刚接触的小白,自然不太明白其中原理,最重要的是,这个玩意如果不明白基 ...

随机推荐

  1. sql通用行列转换

    -- 行转列 select 姓名, SUM(case 课程 when '语文' then 分数 else 0 end) as 语文, SUM(case 课程 when '数学' then 分数 els ...

  2. 从源码角度谈谈MySQL "Too many open files"错误的根本原因

    "Too many open files"是一个比较常见的错误,不仅仅是在 MySQL 中.只要是在 Linux 中启动的进程,都有可能遇到这个错误. 究其原因,是进程打开的文件描 ...

  3. Blazor 数据绑定开发指南

    翻译自 Waqas Anwar 2021年3月21日的文章 <A Developer's Guide to Blazor Data Binding> [1] 现如今,大多数 Web 应用程 ...

  4. C语言:体现\r的结果

    #include <stdio.h> #include <windows.h> //体现\r的结果 //转义字符\r,回车,将光标移动到当前行最开始 main() { char ...

  5. 团队开发day09

    web端的数据成功存储,但是和android端的数据获取到的数据不适配, 进行数据之间的适配. 遇到问题:在安卓中glide不能解析png图片,于是修改二进制流的转化,将png先转化为jpg 再存入到 ...

  6. js学习-es6实现枚举

    最近大部分时间再写dart,突然用到js,发现js不能直接声明一个枚举.搜索发现还是有实现的方式,于是总结一下. 目录 枚举特点 Object.freeze() Symbol 实现 体现不可更改 体现 ...

  7. 【论文阅读】PRM-RL Long-range Robotic Navigation Tasks by Combining Reinforcement Learning and Sampling-based Planning

    目录 摘要部分: I. Introduction II. Related Work III. Method **IMPORTANT PART A. RL agent training [第一步] B. ...

  8. 【LeetCode】523. 连续的子数组和

    523. 连续的子数组和 知识点:数组:前缀和: 题目描述 给你一个整数数组 nums 和一个整数 k ,编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组: 子数组大小 至少为 2 ,且 ...

  9. 什么是ETL--ETL定义、过程和工具选型思路

    ETL代表"提取.转换和加载".ETL 过程在数据集成策略中起着关键作用.ETL允许企业从多个来源收集数据并将其整合到一个集中的位置.ETL还使不同类型的数据可以协同工作. 概述 ...

  10. 【Azure 应用服务】App Service 通过配置web.config来添加请求返回的响应头(Response Header)

    问题描述 在Azure App Service上部署了站点,想要在网站的响应头中加一个字段(Cache-Control),并设置为固定值(Cache-Control:no-store) 效果类似于本地 ...