创建网站需要什么平台,无锡网站建设外包优势,站长之家源码下载,怎么查有做网站的公司前言博客园#xff08;cnblogs.com#xff09;中有很多关于 .NET async/await 的介绍#xff0c;但是很遗憾#xff0c;很少有正确的#xff0c;甚至说大多都是“从现象编原理”都不过分。最典型的比如通过前后线程 ID 来推断其工作方式、在 async 方法中用 Thread.Sleep … 前言博客园cnblogs.com中有很多关于 .NET async/await 的介绍但是很遗憾很少有正确的甚至说大多都是“从现象编原理”都不过分。最典型的比如通过前后线程 ID 来推断其工作方式、在 async 方法中用 Thread.Sleep 来解释 Task 机制而导出多线程模型的结论、在 Task.Run 中包含 IO bound 任务来推出这是开了一个多线程在执行任务的结论等等。看上去似乎可以解释的通可是很遗憾无论是从原理还是结论上看都是错误的。要了解 .NET 中的 async/await 机制首先需要有操作系统原理的基础否则的话是很难理解清楚的如果没有这些基础而试图向他人解释大多也只是基于现象得到的错误猜想。初看异步说到异步大家应该都很熟悉了2012 年 C# 5 引入了新的异步机制Task并且还有两个新的关键字 await 和 async这已经不是什么新鲜事了而且如今这个异步机制已经被各大语言借鉴如 JavaScript、TypeScript、Rust、C 等等。下面给出一个简单的对照语言调度单位关键字/方法C#Task、ValueTaskasync、awaitCstd::futureco_awaitRuststd::future::Future.awaitJavaScript、TypeScriptPromiseasync、await当然这里这并不是本文的重点只是提一下方便大家在有其他语言经验的情况下如果有可以认识到 C# 中 Task 和 async/await 究竟是一个和什么可以相提并论的东西。多线程编程在该异步编程模型诞生之前多线程编程模型是很多人所熟知的。一般来说开发者会使用 Thread、std::thread 之类的东西作为线程的调度单位来进行多线程开发每一个这样的结构表示一个对等线程线程之间采用互斥或者信号量等方式进行同步。多线程对于科学计算速度提升等方面效果显著但是对于 IO 负荷的任务例如从读取文件或者 TCP 流大多数方案只是分配一个线程进行读取读取过程中阻塞该线程void Main(){ while (true) { var client socket.Accept(); new Thread(() ClientThread(client)).Start(); }} void ClientThread(Socket client){ var buffer new byte[1024]; while (...) { // read and block client.Read(buffer, 0, 1024); }}上述代码中Main 函数在接收客户端之后即分配了一个新的用户线程用于处理该客户端从客户端接收数据。client.Read() 执行后该线程即被阻塞即使阻塞期间该线程没有任何的操作该用户线程也不会被释放并被操作系统不断轮转调度这显然浪费了资源。另外如果线程数量多起来频繁在不同线程之间轮转切换上下文线程的上下文也不小会浪费掉大量的性能。异步编程因此对于此工作内容IO我们在 Linux 上有了 epoll/io_uring 技术在 Windows 上有了 IOCP 技术用以实现异步 IO 操作。这里插句题外话吐槽一句Linux 终于知道从 Windows 抄作业了。先前的 epoll 对比 IOCP 简直不能打被 IOCP 全面打压io_uring 出来了才好不容易能追上 IOCP不过 IOCP 从 Windows Vista 时代开始每一代都有很大的优化io_uring 能不能追得上还有待商榷这类 API 有一个共同的特性就是在操作 IO 的时候调用方控制权被让出等待 IO 操作完成之后恢复先前的上下文重新被调度继续运行。所以表现就是这样的假设我现在需要从某设备中读取 1024 个字节长度的数据于是我们将缓冲区的地址和内容长度等信息封装好传递给操作系统之后我们就不管了读取什么的让操作系统去做就好了。操作系统在内核态下利用 DMA 等方式将数据读取了 1024 个字节并写入到我们先前的 buffer 地址下然后切换到用户态将从我们先前让出控制权的位置对其进行调度使其继续执行。你可以发现这么一来在读取数据期间就没有任何的线程被阻塞也不存在被频繁调度和切换上下文的情况只有当 IO 操作完成之后才会被重新调度并恢复先前让出控制权时的上下文使得后面的代码继续执行。当然这里说的是操作系统的异步 IO 实现方式以便于读者对异步这个行为本身进行理解和 .NET 中的异步还是有区别Task 本身和操作系统也没什么关系。Task (ValueTask)说了这么久还是没有解释 Task 到底是个什么东西从上面的分析就可以得出Task 其实就是一个所谓的调度单位每个异步任务被封装为一个 Task 在 CLR 中被调度而 Task 本身会运行在 CLR 中的预先分配好的线程池中。总有很多人因为 Task 借助线程池执行而把 Task 归结为多线程模型这是完全错误的。这个时候有人跳出来了说你看下面这个代码12345678static async Task Main(){ while (true) { Console.WriteLine(Environment.CurrentManagedThreadId); await Task.Delay(1000); }}输出的线程 ID 不一样欸你骗人这明明就是多线程对于这种言论我也只能说这些人从原理上理解的就是错误的。当代码执行到 await 的时候此时当前的控制权就已经被让出了当前线程并没有在阻塞地等待延时结束待 Task.Delay() 完毕后CLR 从线程池当中挑起了一个先前分配好的已有的但是空闲的线程将让出控制权前的上下文信息恢复使得该线程恰好可以从先前让出的位置继续执行下去。这个时候可能挑到了先前让出前所在的那个线程导致前后线程 ID 一致也有可能挑到了另外一个和之前不一样的线程执行下面的代码使得前后的线程 ID 不一致。在此过程中并没有任何的新线程被分配了出去。在 .NET 中由于采用 stackless 的做法这里需要用到 CPS 变换大概是这么个流程using System;using System.Threading.Tasks; public class C{ public async Task M() { var a 1; await Task.Delay(1000); Console.WriteLine(a); }}编译后public class C{ [StructLayout(LayoutKind.Auto)] [CompilerGenerated] private struct Md__0 : IAsyncStateMachine { public int 1__state; public AsyncTaskMethodBuilder t__builder; private int a5__2; private TaskAwaiter u__1; private void MoveNext() { int num 1__state; try { TaskAwaiter awaiter; if (num ! 0) { a5__2 1; awaiter Task.Delay(1000).GetAwaiter(); if (!awaiter.IsCompleted) { num (1__state 0); u__1 awaiter; t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); return; } } else { awaiter u__1; u__1 default(TaskAwaiter); num (1__state -1); } awaiter.GetResult(); Console.WriteLine(a5__2); } catch (Exception exception) { 1__state -2; t__builder.SetException(exception); return; } 1__state -2; t__builder.SetResult(); } void IAsyncStateMachine.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext this.MoveNext(); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { t__builder.SetStateMachine(stateMachine); } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine this.SetStateMachine(stateMachine); } } [AsyncStateMachine(typeof(Md__0))] public Task M() { Md__0 stateMachine default(Md__0); stateMachine.t__builder AsyncTaskMethodBuilder.Create(); stateMachine.1__state -1; stateMachine.t__builder.Start(ref stateMachine); return stateMachine.t__builder.Task; }}可以看到原来的变量 a 被塞到了 a5__2 里面去相当于备份上下文Task 状态的转换后也是靠着调用 MoveNext相当于状态转换后被重新调度来接着驱动代码执行的里面的 num 就表示当前的状态num 如果为 0 表示 Task 完成了于是接着执行下面的代码 Console.WriteLine(a5__2);。当然在 WPF 等地方因为利用了 SynchronizationContext 对调度行为进行了控制所以可以得到和上述不同的结论和这个相关的还有 .ConfigureAwait() 的用法但是这里不是本文重点因此就不做展开。但是上面和经典的多线程编程的那一套一样吗?不一样。至于 ValueTask 是个什么玩意官方发现Task 由于本身是一个 class在运行时如果频繁反复的分配和回收会给 GC 造成不小的压力因此出了一个 ValueTask这个东西是 struct分配在栈上这样的话就不会给 GC 造成压力了减轻了开销。不过也正因为 ValueTask 是会在栈上分配的值类型结构因此提供的功能也不如 Task 全面。Task.Run由于 .NET 是允许有多个线程的因此也提供了 Task.Run 这个方法允许我们将 CPU bound 的任务放在上述的线程池之中的某个线程上执行并且允许我们将该负载作为一个 Task 进行管理仅在这一点才和多线程的采用线程池的编程比较像。对于浏览器环境v8这个时候是完全没有多线程这一说的因此你开的新的 Promise 其实是后面利用事件循环机制将该微任务以异步的方式执行。想一想在 JavaScript 中Promise 是怎么用的123456789101112let p new Promise((resolve, reject) { // do something let success true; let result 123456; if (success) { resolve(result); } else { reject(failed); }})然后调用12let r await p;console.log(r); // 输出 123456你只需要把这一套背后的驱动器事件循环队列替换成 CLR 的线程池就差不多是 .NET 的 Task 相对 JavaScript 的 Promise 的工作方式了。如果你把 CLR 线程池线程数量设置为 1那就和 JavaScript 这套几乎差不多了虽然实现上还是有差异。这时有人要问了“我在 Task.Run 里面套了好几层 Task.Run可是为什么层数深了之后里面的不执行了呢?” 这是因为上面所说的线程池被耗尽了后面的 Task 还在排着队等待被调度。自己封装异步逻辑了解了上面的东西之后相信对 .NET 中的异步机制应该理解得差不多了可以看出来这一套是名副其实的 coroutine并且在实现上是 stackless 的。至于有的人说的什么状态机什么的只是实现过程中利用的手段而已并不是什么重要的东西。那我们要怎么样使用 Task 来编写我们自己的异步代码呢?事件驱动其实也可以算是一种异步模型例如以下情景A 函数调用 B 函数调用发起后就直接返回不管了BeginInvokeB 函数执行完成后触发事件执行 C 函数。private event Action CompletedEvent; void A(){ CompletedEvent C; Console.WriteLine(begin); ((Action)B).BeginInvoke();} void B(){ Console.WriteLine(running); CompletedEvent?.Invoke();} void C(){ Console.WriteLine(end);}那么我们现在想要做一件事就是把上面的事件驱动改造为利用 async/await 的异步编程模型改造后的代码就是简单的async Task A(){ Console.WriteLine(begin); await B(); Console.WriteLine(end);} Task B(){ Console.WriteLine(running); return Task.CompletedTask;}你可以看到原本 C 函数的内容被放到了 A 调用 B 的下面为什么呢?其实很简单因为这里 await B(); 这一行以后的内容本身就可以理解为 B 函数的回调了只不过在内部实现上不是直接从 B 进行调用的回调而是 A 先让出控制权B 执行完成后CLR 切换上下文将 A 调度回来继续执行剩下的代码。如果事件相关的代码已经确定不可改动即不能改动 B 函数我们想将其封装为异步调用的模式那只需要利用 TaskCompletionSource 即可private event Action CompletedEvent; async Task A(){ // 因为 TaskCompletionSource 要求必须有一个泛型参数 // 因此就随便指定了一个 bool // 本例中其实是不需要这样的一个结果的 // 需要注意的是从 .NET 5 开始 // TaskCompletionSource 不再强制需要泛型参数 var tsc new TaskCompletionSourcebool(); // 随便写一个结果作为 Task 的结果 CompletedEvent () tsc.SetResult(false); Console.WriteLine(begin); ((Action)B).BeginInvoke(); await tsc.Task; Console.WriteLine(end);} void B(){ Console.WriteLine(running); CompletedEvent?.Invoke();}顺便提一句这个 TaskCompletionSourceT 其实和 JavaScript 中的 PromiseT 更像。SetResult() 方法对应 resolve()SetException() 方法对应 reject()。.NET 比 JavaScript 还多了一个取消状态因此还可以 SetCancelled() 表示任务被取消了。同步方式调用异步代码说句真的一般能有这个需求都说明你的代码写的有问题但是如果你无论如何都想以阻塞的方式去等待一个异步任务完成的话12Task t ...t.GetAwaiter().GetResult();祝你好运这相当于t 中的异步任务开始执行后你将当前线程阻塞然后等到 t 完成之后再唤醒可以说是毫无意义而且很有可能因为代码编写不当而导致死锁的发生。void async 是什么?最后有人会问了函数可以写 async Task Foo()还可以写 async void Bar()这有什么区别呢?对于上述代码我们一般调用的时候分别这么写12await Foo();Bar();可以发现诶这个 Bar 函数不需要 await 诶。为什么呢?其实这和用以下方式调用 Foo 是一样的1_ Foo();换句话说就是调用后瞬间就直接抛掉不管了不过这样你也就没法知道这个异步任务的状态和结果了。await 必须配合 Task/ValueTask 才能用吗?当然不是。在 C# 中只要你的类中包含 GetAwaiter() 方法和 bool IsCompleted 属性并且 GetAwaiter() 返回的东西包含一个 GetResult() 方法、一个 bool IsCompleted 属性和实现了 INotifyCompletion那么这个类的对象就是可以 await 的。public class MyTaskT{ public MyAwaiterT GetAwaiter() { return new MyAwaiterT(); }} public class MyAwaiterT : INotifyCompletion{ public bool IsCompleted { get; private set; } public T GetResult() { throw new NotImplementedException(); } public void OnCompleted(Action continuation) { throw new NotImplementedException(); }} public class Program{ static async Task Main(string[] args) { var obj new MyTaskint(); await obj; }}结语本文至此就结束了感兴趣的小伙伴可以多多学习一下操作系统原理对 CLR 感兴趣也可以去研究其源代码https://github.com/dotnet/runtime 。.NET 的异步和线程密不可分但是和多线程编程方式和思想是有本质不同的也希望大家不要将异步和多线程混淆了而这有联系也有区别。从现象猜测本质是大忌可能解释的通但是终究只是偶然现象而且从原理上看也是完全错误的甚至官方的实现代码稍微变一下可能立马就无法解释的通了。总之通过本文希望大家能对异步和 .NET 中的异步有一个更清晰的理解。感谢阅读。