我们提供安全,免费的手游软件下载!

安卓手机游戏下载_安卓手机软件下载_安卓手机应用免费下载-先锋下载

当前位置: 主页 > 软件教程 > 软件教程

深入理解多线程与异步

来源:网络 更新时间:2024-11-30 09:37:46

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

异步适用于"IO密集型"的场景,它可以避免因为线程等待IO形成的线程饥饿,从而造成程序吞吐量的降低。其本质是:让线程的cpu片不再浪费在等待上,期间可以去干其它的事情。要注意的是:Async不能加速程序的执行,它只能做到不阻塞线程。

多线程适用于"CPU密集型",主要是为了更多的利用多核CPU来同时执行逻辑。将一个大任务分而治之,提高完成速度,进而提高程序的并发能力。值得注意的是,如果过多使用线程同步,会降低多线程的使用效果。

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

异步请求流程图(Windows)

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

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

IO完成端口(IO Completion Port)是Windows操作系统的一个内核对象,专门用来解决异步IO的问题,C#中所有异步操作都依赖此端口。其本质是一个发布订阅模式的队列。

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

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

        /// 
        /// 创建IO完成端口
        /// 
        [DllImport("kernel32.dll")]
        static extern nint CreateIoCompletionPort(nint FileHandle, nint ExistingCompletionPort, nint CompletionKey, int NumberOfConcurrentThreads);
        // 其他相关API

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

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

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

            private void Poll()
            {
                //轮询调用GetQueuedCompletionStatusEx,获取IO数据。
                // 其他相关源码
            }

C# 中的异步函数

        //一旦将方法标记为async,编译器就会将代码转换成状态机
        static async void Test()
        {
            // 省略其他代码
        }

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

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

分享几个写的不错的博文,偷懒一下。核心是MoveNext函数,里面包含了根据状态机status而执行不同代码的模板代码。一个Task最少要被调用两次MoveNext,第一次调用是主动触发初始化状态机,第二次调用是回调函数再次执行状态机。

    public class GetStringAsync : IAsyncStateMachine
    {
        // 省略其他代码
    }

异步方法的异常处理

当异步操作发生异常时,IO Completion Port会告诉程序,异步操作已经完成,但存在一个错误。不会跟常规异常一样直接从内核态抛出一个异常。因此ThreadPool会拿到IRP数据,里面包含了异常信息。它自己也不会抛出来。而是调用SetException存储起来。当你调用await/GetResult() 时才会真正的抛出异常。因为当你没有及时获取Task的异常时,它会被丢弃。你需要妥善处理未抛出的异常。

眼见为实

        internal TResult GetResultCore(bool waitCompletionNotification)
        {
            // 省略其他代码
        }

ValueTask

在众多异步场景中,有些场景是,GetAsync()第一次需要异步IO等待,然后把结果缓存到静态变量里。接下来N次都是不需要异步IO等待的。直接可以同步完成。比如说Entity Framework中的FindAsync().只有第一次会查询数据库,剩下的N次直接读取内存。如果使用Task ,从状态机的源码也可以看到,创建一个Task对象花销不少且为引用类型。创建越多对GC压力越大。

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

它的出现纯粹为了性能。

眼见为实

        public TResult Result
        {
            get
            {
                // 省略其他代码
            }
        }