简介

多线程与异步是两个完全不同的概念,常常有人混淆。

  1. 异步

    异步适用于"IO密集型"的场景,它可以避免因为线程等待IO形成的线程饥饿,从而造成程序吞吐量的降低。

    其本质是:让线程的cpu片不再浪费在等待上,期间可以去干其它的事情。

    要注意的是:Async不能加速程序的执行,它只能做到不阻塞线程。

  2. 多线程

    多线程适用于"CPU密集型",主要是为了更多的利用多核CPU来同时执行逻辑。将一个大任务分而治之,提高完成速度,进而提高程序的并发能力

    值得注意的是,如果过多使用线程同步,会降低多线程的使用效果

在计算机科学中,一个线程指的是在程序中一段连续的逻辑控制流。在业务很复杂的时候,一个线程无法满足现有业务需求,多线程编程就应运而生。

异步请求流程图(Windows)

  1. ReadAsync底层调用win32 API ReadFile
  2. ReadFile分配IRP数据结构(句柄,读取偏移量,用来填充的byte[]),
  3. 然后传递给windows内核中,
  4. windows把IRP添加到硬盘驱动的IRP队列中,线程不再阻塞,立刻返回到线程池中(在此期IRP尚未处理完成)
  5. 读取硬盘数据
  6. 返回硬盘数据并组装IRP数据
  7. 将IRP Enqueue IO Completion Port
  8. ThreadPool轮询Dequeue该端口,提取IRP
  9. 执行回调,如果没有回调这一步直接丢弃IRP数据

异步操作的核心:IO完成端口(IO Completion Port)

IO完成端口(IO Completion Port)是Windows操作系统的一个内核对象,专门用来解决异步IO的问题,C#中所有异步操作都依赖此端口。

其本质是一个发布订阅模式的队列

CLR在初始化时,创建一个IO Completion Port完成与硬件设备的绑定,使得硬件的驱动程序知道将IRP送到哪里去。

眼见为实:IO Completion Port真的存在吗?

        /// <summary>
/// 创建IO完成端口
/// </summary>
[DllImport("kernel32.dll")]
static extern nint CreateIoCompletionPort(nint FileHandle, nint ExistingCompletionPort, nint CompletionKey, int NumberOfConcurrentThreads); /// <summary>
/// IO数据入列
/// </summary>
[DllImport("kernel32.dll")]
static extern bool PostQueuedCompletionStatus(nint CompletionPort, int dwNumberOfBytesTransferred, nint dwCompletionKey, nint lpOverlapped); /// <summary>
/// IO数据出列
/// </summary>
[DllImport("kernel32.dll")]
static extern bool GetQueuedCompletionStatusEx(nint CompletionPort, out uint lpNumberOfBytes, out nint lpCompletionKey, out nint lpOverlapped, uint dwMilliseconds);

有兴趣的小伙伴可以玩一玩这个api.

眼见为实:异步API真的基于IO Completion Port吗?

众所周知,Task的底层是ThreadPool,那么答案一定在ThreadPool的源码中

No BB,上源码,IOCompletionPoller.Poll

            private void Poll()
{
//轮询调用GetQueuedCompletionStatusEx,获取IO数据。
while (
Interop.Kernel32.GetQueuedCompletionStatusEx(
_port,
_nativeEvents,
NativeEventCapacity,
out int nativeEventCount,
Timeout.Infinite,
false))
{
for (int i = 0; i < nativeEventCount; ++i)
{
Interop.Kernel32.OVERLAPPED_ENTRY* nativeEvent = &_nativeEvents[i];
if (nativeEvent->lpOverlapped != null) // shouldn't be null since null is not posted
{
//把event事件和数据压入内部ConcurrentQueue队列,缓存起来。
//.net 8之前的版本,直接就在这里执行回调了
_events.BatchEnqueue(new Event(nativeEvent->lpOverlapped, nativeEvent->dwNumberOfBytesTransferred));
}
}
//压入线程池的highPriorityWorkItems队列
//.net 8之后,由线程池执行回调
_events.CompleteBatchEnqueue();
} ThrowHelper.ThrowApplicationException(Marshal.GetHRForLastWin32Error());
}

C# 中的异步函数

        //一旦将方法标记为async,编译器就会将代码转换成状态机
static async void Test()
{
//线程1进入,初始化client
var httpClient = new HttpClient();
//GetAsync内部分配一个Task对象
var getTask = httpClient.GetAsync("https://www.baidu.com");
//此时, aait操作符实际会在Task对象上调用ContinueWith,向它传递用于恢复状态机的方法,线程线程从Test()方法中返回
//在未来某个时刻,IO Completion Port 完成网络IO入列,线程池通知Task对象,一个新线程会重新进入Test()方法,从await操作符的位置开始执行ContinueWith回调方法(也就是代码后面的内容)。
var response = await getTask;
}

编译器如何将异步函数转换为状态机?

https://www.cnblogs.com/JulianHuang/p/18137189

https://www.cnblogs.com/huangxincheng/p/13558006.html

分享几个写的不错的博文,偷懒一下。

核心是MoveNext函数,里面包含了根据状态机status而执行不同代码的模板代码.

一个Task最少要被调用两次MoveNext,第一次调用是主动触发初始化状态机,第二次调用是回调函数再次执行状态机

    public class GetStringAsync : IAsyncStateMachine
{
public int state;
private string html;
private string taskResult; public AsyncTaskMethodBuilder builder;
private TaskAwaiter<string> awaiter;
/// <summary>
/// 状态机机制
/// </summary>
public void MoveNext()
{
var localState = state;
TaskAwaiter<string> localAwaiter = default(TaskAwaiter<string>);
GetStringAsync localStateMachine; try
{
switch (localState)
{
//第一次初始化 (publish)
case -1:
localAwaiter = Task.Run(() =>
{
Thread.Sleep(1000); //模拟网络IO耗时
var response = "<html><h1>百度</h1></html>";
return response;
}).GetAwaiter();//转为TaskAwaiter对象,内部实现INotifyCompletion接口,使得具备传入回调函数的能力 if (!localAwaiter.IsCompleted)
{
localState = state = 0;
awaiter = localAwaiter;
localStateMachine = this;
builder.AwaitUnsafeOnCompleted(ref localAwaiter, ref localStateMachine);//将当前注册机传入回调函数,当前线程返回线程池
return;
}
break; //第二次异步完成的回调 (subscribe)
case 0:
localAwaiter = awaiter;
awaiter = default(TaskAwaiter<string>);
localState = state = -1;
break;
}
//等价于ContinueWith
taskResult = localAwaiter.GetResult();
html = taskResult;
taskResult = null;
Console.WriteLine($"GetStringAsync方法返回:{html}");
}
catch (Exception ex)
{
state = -2;
html = null;
builder.SetException(ex);//只有调用await/result才会抛出异常,否则会丢弃。
return;
} state = -2;
html = null;
builder.SetResult();
}
}

异步方法的异常处理

当异步操作发生异常时,IO Completion Port会告诉程序,异步操作已经完成,但存在一个错误。不会跟常规异常一样直接从内核态抛出一个异常。

因此ThreadPool会拿到IRP数据,里面包含了异常信息。它自己也不会抛出来。而是调用SetException存储起来。

当你调用await/result 时才会真正的抛出异常。因为当你没有及时获取Task的异常时,它会被丢弃。你需要妥善处理未抛出的异常

        internal TResult GetResultCore(bool waitCompletionNotification)
{
// If the result has not been calculated yet, wait for it.
if (!IsCompleted) InternalWait(Timeout.Infinite, default); // won't throw if task faulted or canceled; that's handled below // Notify the debugger of the wait completion if it's requested such a notification
if (waitCompletionNotification) NotifyDebuggerOfWaitCompletionIfNecessary(); // Throw an exception if appropriate.
if (!IsCompletedSuccessfully) ThrowIfExceptional(includeTaskCanceledExceptions: true); // We shouldn't be here if the result has not been set.
Debug.Assert(IsCompletedSuccessfully, "Task<T>.Result getter: Expected result to have been set."); return m_result!;
}

ValueTask

在众多异步场景中,有些场景是,GetAsync()第一次需要异步IO等待,然后把结果缓存到静态变量里。接下来N次都是不需要异步IO等待的。直接可以同步完成。

比如说Entity Framework中的FindAsync().只有第一次会查询数据库,剩下的N次直接读取内存。

如果使用Task<Result> ,从状态机的源码也可以看到,创建一个Task对象花销不少且为引用类型。创建越多对GC压力越大。

为了减少这种场景下的性能消耗,可以使用ValueTask,它为结构体值类型,正常不需要从托管堆中分配内存。

  1. 如果异步操作不需要等待,可以同步完成,那么回调会被立刻调用,没有多余开销。
  2. 如果异步操作需要等待,那依旧会创建一个Task对象

它的出现纯粹为了性能。

眼见为实

上源码

System.Private.CoreLib\src\System\Runtime\CompilerServices\ValueTaskAwaiter.cs

        public TResult Result
{
get
{
object? obj = _obj;//Task对象 if (obj == null)//Task完成后会置为null,大家猜一猜为什么要置为空?
{
return _result!;//直接返回缓存的结果
} if (obj is Task<TResult> t) //Task未完成,还是走Task逻辑不变
{
TaskAwaiter.ValidateEnd(t);
return t.ResultOnSuccess;
} return Unsafe.As<IValueTaskSource<TResult>>(obj).GetResult(_token);//去IValueTaskSource里找缓存的result
}
}

.NET Core 异步(Async)底层原理浅谈的更多相关文章

  1. Java线上问题排查神器Arthas快速上手与原理浅谈

    前言 当你兴冲冲地开始运行自己的Java项目时,你是否遇到过如下问题: 程序在稳定运行了,可是实现的功能点了没反应. 为了修复Bug而上线的新版本,上线后发现Bug依然在,却想不通哪里有问题? 想到可 ...

  2. CSRF漏洞原理浅谈

    CSRF漏洞原理浅谈 By : Mirror王宇阳 E-mail : mirrorwangyuyang@gmail.com 笔者并未深挖过CSRF,内容居多是参考<Web安全深度剖析>.& ...

  3. .Net Core异步async/await探索

    走进.NetCore的异步编程 - 探索 async/await 前言: 这段时间开始用.netcore做公司项目,发现前辈搭的框架通篇运用了异步编程方式,也就是async/await方式,作为一个刚 ...

  4. JAVA CAS原理浅谈

    java.util.concurrent包完全建立在CAS之上的,没有CAS就不会有此包.可见CAS的重要性. CAS CAS:Compare and Swap, 翻译成比较并交换. java.uti ...

  5. 如何把Java代码玩出花?JVM Sandbox入门教程与原理浅谈

    在日常业务代码开发中,我们经常接触到AOP,比如熟知的Spring AOP.我们用它来做业务切面,比如登录校验,日志记录,性能监控,全局过滤器等.但Spring AOP有一个局限性,并不是所有的类都托 ...

  6. CAS+SSO原理浅谈

    http://www.cnblogs.com/yonsin/archive/2009/08/29/1556423.htmlSSO 是一个非常大的主题,我对这个主题有着深深的感受,自从广州 UserGr ...

  7. php模板原理PHP模板引擎smarty模板原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  8. PHP的模板引擎smarty原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  9. Docker 基础底层架构浅谈

    docker学习过程中,免不了需要学习下docker的底层技术,今天我们来记录下docker的底层架构吧! 从上图我们可以看到,docker依赖于linux内核的三个基本技术:namespaces.C ...

  10. Java中的SPI原理浅谈

    在面向对象的程序设计中,模块之间交互采用接口编程,通常情况下调用方不需要知道被调用方的内部实现细节,因为一旦涉及到了具体实现,如果需要换一种实现就需要修改代码,这违反了程序设计的"开闭原则& ...

随机推荐

  1. 【笔记】Tapable源码解析图以及webpack怎样实现一个插件plugin

    Tapable源码解析图,如图所示: 一个webpack plugin由一下几个步骤组成: 一个JavaScript类函数. 在函数原型 (prototype)中定义一个注入compiler对象的ap ...

  2. EF Core – Unit of Work, DbContext, Transaction 概念解释

    前言 踩了一个坑, 下面是 2 个 scope 的调用, 第 1 和 3 是一个 Audit log filter action, 第 2 个是 controller. // open tran // ...

  3. 禁用QQ自动升级(实测可用)

    事件起因: 某客户电脑QQ从怀旧版(9.7)自动升级到NT版本(9.9)版本,新版本由于消息通知功能不能直接查看,老版本右下角消息闪烁,鼠标放上去便可以看到有几个联系人的消息,因此客户强烈要求版本回退 ...

  4. C#/.NET/.NET Core开发实战教程集

    DotNetGuide介绍 DotNetGuide是一个专注于C#/.NET/.NET Core学习.工作.面试指南的GitHub知识库,该知识库在GitHub中Star数已突破6.5k+当然这离不开 ...

  5. 简单粗暴的实现 Blazor Server 登录鉴权

    既然是简单粗暴,那么就不用关心诸如 IDentityServer4,OAuth 之类的组件,也不使用 AuthenticationStateProvider.IAuthService, razor 页 ...

  6. SMMU中stage1 和stage2 的意思

    ARM SMMU(System Memory Management Unit)是一种用于ARM架构的内存管理单元,它支持两阶段的地址转换机制,即Stage 1和Stage 2.这种机制允许操作系统和虚 ...

  7. Android应用启动全流程分析(源码深度剖析)

    目录 1.前言 2.大纲 3. Input触控事件处理流程 3.1 系统机制分析 3.2 结合Systrace分析 4. 应用进程的创建与启动 4.2 创建应用进程 4.2.1 AMS 发送socke ...

  8. 推荐一款轻量级 eBPF 前端工具 ply

    1 Overview ply 是 eBPF 的 front-end 前端工具之一,专为 embedded Linux systems 开发,采用 C 语言编写,只需 libc 和内核支持 BPF 就可 ...

  9. 墨天轮访谈 | 叶金荣:GreatSQL开源社区——做中国广受欢迎的开源数据库

    分享嘉宾:叶金荣 万里数据库开源生态负责人 整理:墨天轮社区 导读 大家好,我是来自万里数据库的叶金荣,GreatSQL开源社区的愿景是做中国广受欢迎的开源数据库,这也是我今天分享的主题. Great ...

  10. C#内存缓存

    把缓存数据放到应用程序的内存,内存缓存中保存的是一些列键值对 :生命周期就是关闭程序的时候,内存数据就会被销毁 : IMemerCache  接口 Get 获取缓存 Set 设置缓存