简介

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

  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. ZEGO 即构科技首发适配鸿蒙系统的 Express SDK 1.0 版本

    ​ 2019年8月,华为在开发者大会上正式发布鸿蒙系统. HarmonyOS 鸿蒙系统是一款"面向未来".面向全场景(移动办公.运动健康.社交通信.媒体娱乐等)的分布式操作系统.在 ...

  2. 704 二分查找 golang实现

    二分查找(Binary Search)是一种高效的查找算法,适用于 有序数组 或 有序列表.它的基本思想是通过将搜索范围逐渐缩小到目标元素所在的一半,从而大大减少查找的次数. 二分查找的基本原理 排序 ...

  3. ASP.NET Core – 操作 Uri 和 Query

    前言 以前就有写过了 Asp.net core 学习笔记 (操作 URL 和 Query), 但很乱, 这篇作为整理. Uri 介绍 结构: [Scheme]://[Host]:[Port][/Pat ...

  4. 微信小程序开发疑难

    1.开发者工具在小程序webview中注入wx时会提示token过期,但真机正常

  5. 暑假集训CSP提高模拟1

    A.Start 比较小的大模拟,还没改出来 B.mine 线性推一下(这个题记搜容易写偏,因为分讨太多) 设 \(f[i][j]\),第一维表示位置,第二位表示末位状态(是雷,是 \(0\),是 \( ...

  6. 前端VUE调用后台接口,实现基本增删改查

    设置接口请求 作为一个后台,个人一点感想:前端现在都是组件化开发,会看文档基本功能就能实现. js文件 import request from '@/router/axios' // 查询 expor ...

  7. SXYZ-6.28训练赛

    今天上午出中考成绩,所以下午打了一场训练赛,只有两个小时,没有昨天毒瘤,但也很毒瘤(还是模拟赛好) 关于中考可以看我的中考游记 (为了保护隐私,以后都把姓名涂掉) 为什么还是倒数啊~ T1 binar ...

  8. 【赵渝强老师】Kafka的持久化

    一.Kafka持久化概述 Kakfa 依赖文件系统来存储和缓存消息.对于硬盘的传统观念是硬盘总是很慢,基于文件系统的架构能否提供优异的性能?实际上硬盘的快慢完全取决于使用方式.同时 Kafka 基于 ...

  9. WeiXin.Export.20220726

    用 QuestPDF操作生成PDF更快更高效! Blazor Server 应用程序中进行 HTTP 请求 开源WPF控件库-AdonisUI FastTunnel-开源内网穿透框架 AI 之 Ope ...

  10. 三维医学图像数据扩充:flip and rotate

    对于小数据量医学图像进行深度学习使,会由于数据量过小而过拟合.因此我们需要采用数据扩充方法,而flip和rotate又是经常用到的,这里做一个简单的实现. 输入为[batchsize,height, ...