单线程,多线程,异步,同步详解

张开发
2026/4/11 7:23:08 15 分钟阅读

分享文章

单线程,多线程,异步,同步详解
c# 异步代码是如何解决高并发问题的async/await、Task、IOCP/epoll_c#高并发三种解决方法-CSDN博客C#编程高并发的几种处理方法 - zock - 博客园 (cnblogs.com)关于异步与多线程笔者在刚接触的时候一直存在诸多疑惑甚至一度以为这俩概念是用来描述同一种技术在不同场景下的应用进而导致对很多与它们相关的概念都一知半解代码中的async/await关键词也是莫名其妙地在用。但是在不断地接触这类概念因为现在的场景中异步与多线程几乎无处不在的过程中还是不断地修正了这种思维。代码写起来也顺手多了。所以这篇文章也是有感而发在去年的时间里因为多线程和异步踩了不少雷希望能够给大家做一点简单的解释和区分把。TL, DR: 请参照文章最后的例子 多线程是什么多线程 技术有时又称 并行 技术就是同时做多件事情。这十分好理解也很直观。现在的CPU都不止有一个核每个核都至少具备一个线程某些CPU具备超线程能力一个核可以具备多个线程打开Windows自带的任务管理器切到性能一栏选中CPU线程总数显示在“逻辑处理器”部分。可以看到笔者这颗 i9-14900HX具备32个线程。###########################################################################32 逻辑处理器调度 6990 线程的完整机制解析我用最通俗、工程化的方式把CPU 核心、线程、进程的调度原理结合你当前的系统状态32 逻辑核、312 进程、6990 线程讲透同时解释为什么 32 核能跑数千线程。一、核心概念先理清避免混淆表格术语定义你当前系统的状态逻辑处理器Logical ProcessorCPU 可调度的最小硬件执行单元对应超线程后的线程数32 个i9-14900HX8P 核 16E 核P 核超线程→16 线程E 核无超线程→16 线程合计 32线程Thread操作系统调度的最小执行单元是代码执行的载体6990 个所有进程的线程总和进程Process资源分配的最小单位包含 1 个或多个线程共享进程内存空间312 个系统 应用的独立运行程序二、核心机制32 核如何跑数千线程1. 本质时间片轮转调度Time-Slicing这是操作系统调度的核心原理也是 32 核能跑数千线程的根本原因硬件限制同一时刻32 个逻辑核最多只能同时执行 32 个线程1 核 1 线程软件调度操作系统Windows会把 CPU 时间切成极短的「时间片」通常 1~10ms快速在数千个线程之间切换用户感知切换速度极快毫秒级人眼完全无法察觉看起来就像所有线程在「同时运行」2. 调度流程Windows 内核调度器plaintext线程就绪队列6990个线程排队 ↓ 调度器按优先级分配时间片 ↓ 32个逻辑核同时执行32个线程时间片内 ↓ 时间片用完 → 线程回到就绪队列 ↓ 调度下一批32个线程执行 ↓ 循环往复无限轮转三、关键调度细节Windows 专属机制1. 混合架构调度i9-14900HX 专属你的 CPU 是性能核 (P 核) 能效核 (E 核) 混合架构Windows 会做智能调度P 核8 核 16 线程优先分配给高优先级、计算密集型任务如工业视觉算法、编译、游戏E 核16 核 16 线程分配给后台任务、低优先级线程如系统服务、后台更新、UI 线程调度器Intel Thread Director Windows 内核协同自动把线程分配到最合适的核心避免 P 核被后台任务占用2. 进程与线程的关系进程是「容器」线程是「执行体」312 个进程里每个进程至少有 1 个主线程多数进程有多个工作线程如浏览器、IDE、工业软件线程共享进程资源同一进程的线程共享内存、句柄切换成本远低于进程切换你当前的 6990 线程平均每个进程约 22.4 个线程属于 Windows 正常范围系统服务、后台应用会创建大量线程3. 线程优先级调度Windows 给线程分配 0~31 的优先级调度规则高优先级线程优先抢占 CPU 时间片先执行低优先级线程在高优先级线程空闲时执行饥饿预防调度器会动态提升长期未执行线程的优先级避免永远得不到执行四、为什么 32 核跑 6990 线程不会卡死1. 绝大多数线程处于「等待状态」线程不是一直需要 CPU比如等待 IO磁盘、网络、硬件响应、等待事件、睡眠的线程会进入阻塞状态不占用 CPU 时间片你当前 CPU 利用率仅 7%说明 93% 的时间里32 个核都在空闲数千线程大部分时间在等待只有少量线程在执行真正需要 CPU 的线程通常只有几十到几百个远低于 32 核的并行能力2. 线程切换成本极低线程上下文切换保存 / 恢复寄存器、栈的耗时仅纳秒级对系统性能影响极小只有当线程数远超 CPU 核数如数万线程且全部为 CPU 密集型时才会出现「切换开销 执行开销」的 thrashing颠簸现象五、工程实践如何优化多线程调度针对你的工业视觉场景1. 线程数配置最佳实践表格场景推荐线程数原因CPU 密集型如对比度计算、光谱分析等于逻辑核数32避免切换开销最大化并行效率IO 密集型如相机采图、网络通信2~4 倍逻辑核数64~128线程等待 IO 时其他线程可执行混合负载工业视觉软件1.5~2 倍逻辑核数48~64平衡计算与 IO避免调度拥堵2. 针对你当前系统的优化建议工业视觉算法线程绑定把计算密集型线程如ProcessQueue里的对比度计算绑定到 P 核避免被 E 核调度拖慢csharp运行// C# 线程绑定到P核示例需根据核心编号调整 Process.GetCurrentProcess().ProcessorAffinity new IntPtr(0xFFFF0000); // 绑定到高16位P核限制后台线程数关闭不必要的后台服务减少无效线程对调度器的占用异步 IO 优化用async/await替代同步阻塞线程减少线程等待开销线程池配置自定义线程池大小避免.NET 默认线程池无限制创建线程六、常见误区纠正❌ 误区 1线程数越多性能越高真相线程数超过 CPU 核数后性能会先升后降超过阈值后切换开销会抵消并行收益你的系统6990 线程是系统 应用的总和不是同时执行的线程数绝大多数在等待❌ 误区 232 核只能跑 32 个线程真相32 核是同时执行的最大线程数操作系统通过时间片轮转支持数千甚至数万个线程并发❌ 误区 3进程数 线程数真相进程是资源容器线程是执行单元1 个进程可以有 N 个线程两者无直接对应关系七、总结32 逻辑处理器调度 6990 线程的核心是时间片轮转 优先级调度 混合架构智能分配绝大多数线程处于等待状态真正执行的线程数远低于 32因此系统负载极低7%工业视觉场景下需针对性优化线程数和核心绑定最大化算法执行效率###########################################################################每个线程可以看作是一个流水线有多个流水线就可以同时运行多段代码对于某些计算量巨大、同时计算任务又可以拆分的代码可以将计算任务分配到各个流水线上去这样就能够更高效地完成指定任务。总而言之多线程即 “同时做多件事情”。下面的代码是一个简单的多线程例子。运行这段代码发现最后打印的总耗时比每段加起来的耗时要少这就是并行计算的结果。读者感兴趣可以自行把foreach循环中的有关Task类和lambda函数封包去掉直接每段运行再进行总耗时求和。ListTask tasks new ListTask(); Stopwatch sw Stopwatch.StartNew(); foreach (var item in Enumerable.Range(0,3)) { tasks.Add(Task.Run( // lambda函数体 () { Stopwatch sw Stopwatch.StartNew(); Thread.Sleep(500); Console.WriteLine(${sw.ElapsedMilliseconds}ms cost); } )); } // 等待所有线程退出 Task.WaitAll(tasks.ToArray()); // 打印计算总耗时 Console.WriteLine(string.Format(Total cost: {0}ms, sw.ElapsedMilliseconds));异步是什么异步是一种任务执行的机制或者说方式它的目的在于解决I/O等待会阻塞线程这个问题最常见的就是GUI线程阻塞造成画面卡顿它的实现依托于硬件底层的IRP(I/O Request Packet)它的本质其实是回调。我可以使用比如 ReadAsync Task.ContinueWith 的组合来实现一个异步实践而更简单的方式是通过微软后续推出 .net4.5 的async-await这套关键词来实践。“异步” 这个概念是对应于 “同步” 概念而言的。“同步”的意思是所有代码从头至尾按顺序逐条执行在一行代码执行完之前不能执行后面的所有代码。下面的例子的中当Sum()函数被调用的时候for循环之后的打印 sum 和 Hello World 一定需要等到这个循环结束之后才能被执行。int Sum(int target) { int sum 0; for (int ind 0; ind target; ind) { sum ind; } Console.WriteLine(sum.ToString()); // 求和结果打印 Console.WriteLine(Hello World!); // Hello World打印 return sum; }而“异步”相对应的就是代码不按从头至尾的顺序执行比如我们如果以某种方式让上面代码示例中的打印 Hello World 在打印 sum 求和结果之前执行那就是异步。实现异步一般是有两种方式其一是通过 多线程 (Multithreading)其二是通过 协程 (Coroutine)。我们平时提到“异步”时更多地是指 “协程异步”。线程异步通过 多线程 来实现异步十分简单直观把要延后执行的部分扔个一个子程序即可。上述例子中把for循环封包在一个lambda函数中然后指派至一个Task类的实例使用这个实例来进行任务管理即可int Sum(int target) { int sum 0; Taskint task Task.Run(() { for (int ind 0; ind target; ind) { sum ind; } Console.WriteLine(sum.ToString()); // 求和结果打印 return sum; }); Console.WriteLine(Hello World!); // Hello World打印 return task.Result; }由于函数封装和线程的指派十分灵活以这种方式实现的异步逻辑在流程控制管理上需要格外小心并且在处理线程返回值、线程之间的通信上需要更加谨慎不留意时很容易造成程序死锁。协程异步协程异步的提出就是为了解决线程异步时需要格外小心程序死锁这个问题。但要提到协程异步不得不说到什么是 “协程” (Coroutine)。协程是什么协程的全程应该被叫做“协程子程序”是“协作式多任务子程序”的另一个名字。“Coroutine” 一词是由Melvin Conway于1958年提出汇编语言新架构时提出的指代“能够随时暂停、恢复的子程序”。在我们学习编程时子程序给我们的初印象一般都是“可被复用的代码片段”它有十分明显的特征仅有一个返回值且仅能返回一次从头至尾执行一旦使用return关键词返回其剩余代码均不再执行两次执行之间的状态无关执行结果仅决定于参数但协程子程序不一样它可以返回多次而不停止执行也可以在返回点恢复执行不从第一行开始执行两次执行之间的状态会互相关联虽给定参数一样但执行结果不一样。如果大家对Python稍有了解的话那一定知道生成器的概念而生成器就是一种协程的架构的实现可以返回多次能够在返回点开始执行而非代码片段头部开始执行可以在代码片段中间通过yield关键词返回其剩余代码会在下次调用时执行两次执行直接的状态有关执行结果不单单仅取决于参数通过协程可以实现许多十分有意思的功能且 所有代码均由一个线程执行。协程如何实现异步协程天然具备“不从头到位按顺序执行”的特性所以可以实现“异步”。下述代码即是通过C#中的生成器来实现“生产者-消费者”、并由主线程作为线程调度者的一个简单异步代码示例生产者和消费者共享一个队列 q消费者每次消费 1 个 q 队列中的对象生产者每次随机生成 0~2 个对象添加至队列 q 中每个调度循环中消费者消费2次生产者生产1次由于使用了随机数生成器每次运行的结果会不一样在Producer和Consumer子程序中每次程序执行时都是从上一次yield关键词后开始执行而非从头开始执行。static IEnumerableobject Producer(Queueint q) // 生产者 { while (true) { if (q.Count 100) { RandomNumberGenerator rng RandomNumberGenerator.Create(); byte[] num new byte[1]; rng.GetBytes(num); int n (int)Math.Round((num[0] / 255.0) * 2); byte[] buff new byte[n]; rng.GetBytes(buff); Console.WriteLine(buff.Aggregate( Produced:[, (s1, s2) s1 $ {s2},) ]); foreach (int item in buff) q.Enqueue(item); Console.WriteLine(q.Aggregate( Queue:[, (s1, s2) s1 $ {s2},) ]); yield return null; // yield返回下次进入时会从此处继续执行 } else if (q null) yield break; else yield return null; } } static IEnumerableobject Consumer(Queueint q) // 消费者 { while (true) { if (q.Count 0) { Console.WriteLine(string.Format( Consumed: {0}, q.Dequeue())); Console.WriteLine(q.Aggregate( Queue:[, (s1, s2) s1 $ {s2},) ]); yield return null; // yield返回下次进入时会从此处继续执行 } else yield break; } } static void Main(string[] args) // 主线程作为调度者 (Dispatcher) { Queueint q new Queueint(); // 共享队列 q Console.WriteLine($Initialization:); Producer(q).GetEnumerator().MoveNext(); // 使用生产者生成初始对象 int maxRunCount 0; while (q.Count 0 maxRunCount 500) // 控制循环 { Console.WriteLine($Loop {maxRunCount}:); Consumer(q).GetEnumerator().MoveNext(); // 消费 Producer(q).GetEnumerator().MoveNext(); // 生产 } }为什么需要异步这个场景我们是常常遇到的我们想要在一个十分耗时的操作结束后更新某UI元素。一般UI是由程序的主线程来维护的在需要执行这个十分耗时的操作时我们可以开启一个子线程去做这件事情并且在子线程结束时对UI进行更新。但正是因为各个对象是由主线程维护的一般不允许子线程直接访问UI元素那么我们在子线程里 无法对UI元素进行更新。于是聪明的我们直接在子线程开启的同时让主线程等待子线程完成这样做的结果就是导致这个主线程等待子线程完成的过程中UI元素会因为主线程在等待而失去对鼠标、键盘事件的响应 —— 窗口处于冻结状态。那么问题就来了如何在进行一个耗时操作时保证主线程不冻结且耗时操作完成后能更新属于主线程的UI元素。此时“异步”一个很重要的概念在这个环境下就十分有用了 —— 乱序执行。下面就是一个使用异步实现读取一个超大文件的一个代码主程序Main()的执行并没有因ReadHeavy()函数的执行而冻结“Read finished” 的打印在子函数ReadHeavy()中子函数被调用的代码是在 “Read file started” 被打印之前但其真正被执行则在其之后且在编写这段程序的程序员手里这段代码仅有一个线程因此这是一个协程异步程序。static async Task Main(string[] args) { var task ReadHeavy(); // 开始文件读取 Console.WriteLine(Read file started.); await task; } async static Task ReadHeavy() { await System.IO.File.ReadAllBytesAsync(E:\Downloads\6_26_2018__2_02_17_PM.tdms); Console.WriteLine(Read finished.); }“6_26_2018__2_02_17_PM.tdms” 是笔者某传感器采集的数据大约有600MB左右的大小算得上一个比较大的文件了而且存储在非SSD磁盘中所以读取时花费的时间会比较多。异步大多数情况下是使用多线程实现的看到这里相信大家心里已经一万个问号了前面大费周章介绍了半天异步不是多线程、异步大多数时候指的“协程异步”怎么到头来又来一个“异步大多数时候是用多线程实现”这难道不是自相矛盾当然不是。这里需要弄清楚的一个很重要的概念 —— 程序员手里的代码与操作系统对处理器硬件的调度执行代码并不是一回事。异步的“协程”是针对于程序员手里的代码而言而目前的编程语言对异步的支持大部分时候是通过多线程来实现的。对于程序员来说代码仅仅执行在一个线程上 —— 这是代码协程构架。对于操作系统/代码编译器而言异步的执行是通过将子程序放入新线程中执行在执行完毕后通知主线程再由主线程来继续执行剩余代码很难理解对不对还是上面文件读取的例子直接上代码async static Task ReadHeavy() { Console.WriteLine($In sub, thread id: {Thread.CurrentThread.ManagedThreadId}); await System.IO.File.ReadAllBytesAsync(E:\Downloads\6_26_2018__2_02_17_PM.tdms); Console.WriteLine(Read finished.); Console.WriteLine($In sub, after read, id: {Thread.CurrentThread.ManagedThreadId}); } static async Task Main(string[] args) { Console.WriteLine(string.Format(Current Thread: {0}, Thread.CurrentThread.ManagedThreadId)); var dummy ReadHeavy(); Console.WriteLine(Read file started.); Console.WriteLine($In main: {Thread.CurrentThread.ManagedThreadId}); await dummy; Console.WriteLine($After await, in main: {Thread.CurrentThread.ManagedThreadId}); Console.ReadKey(); }为了能够看清楚到底是哪个线程执行了代码笔者在之前的代码里加入了大量的打印当前线程的操作。执行结果如下可见在 “Read finished.” 打印结束之后线程编号变了即便是最后在Main()中的打印也跟着变了。情况是这样的主线程进入Main()由于遇到var dummy ReadyHeavy()主线程进入ReadHeavy()函数主线程执行打印函数打印 “In sub, …” 至控制台【关键点来了】 主线程遇到 await 关键词主线程直接返回并将 ReadyAllBytesAsync() 函数交给后台某子线程执行。主线程由ReadHeavy()返回后继续按顺序执行打印 “Read file started.”以及 “In main: …”主线程遇到 await 关键词由于这已经是最顶层函数Main()因此无法返回此时主线程进入等待此时由子线程执行的 ReadyAllBytesAsync() 完成子线程继续执行后续打印 “Read finished.” 以及 “In sub, after read, …”子线程遇到 ReadHeavy() 函数的尾部结束执行函数并通知一直在等待的主线程【关键点又来】主线程收到子线程发来的贺电直接退出将Main()及其所有资源交由子线程处理此时这个子线程“升级”为新的主线程负责执行后续代码相信看到这里大家已经明白了为什么整个程序员代码中仅仅只有一个线程因为除了主线程之外代码编写者根本无需关心其他线程整体对于代码编写者而言其仅仅“感知”到一个线程的存在这是标准的协程异步。而在底层的实现中操作系统的的确确是调用了另一个线程去执行程序中“异步”部分的代码。但是很巧妙的是在异步执行结束时原来的主线程直接被子线程取而代之给人的感觉上是仅有一个线程在做所有的事情且主线程也一直都可以响应事件。这也是为什么上文中一直在使用“一个线程”而非“同一个线程”措辞的缘由。async/await关键词的配对出现就是用来告诉编译器这种异步的情况通过async来表明这个函数是可以从中间返回也可以从中间开始继续执行而await关键词来表明这是一个函数的“暂停”点。一个现实生活中稍微有点形象的例子作为总结笔者举个现实生活中一个例子 —— 银行的工作窗口来说明这一切的一切的区别。假设我现在去银行柜台窗口办业务一个柜员接待了我这个柜员就可以看作是主线程UI在负责跟我用户进行互动。我提出了一个需要取20万现金的请求由于数额比较大需要有人清点现钞。【单线程】此时柜员直接自己去清点花了15分钟然后把钱给我中间这15分钟我被晾在了一边我对着一个空的窗口十分尴尬。花费15分钟拿到现钞。【多线程死锁】此时柜员喊了3位同事四个人一起清点花了5分钟然后他们四个同时把自己点好的那一部分试图递给我但是因为窗口太小他们四个为了争着第一个给我而产生了争执并且一直都没有吵出来个结果我一直被晾在一边。一直没能拿到现钞。【多线程合理管理】此时柜员喊了3位同事四个人一起清点花了5分钟由于提前商量好了他们把点完的钱给其中之前接待我的那位柜员然后这位柜员把钱递到了我手中。但这个过程中我仍然对着一个空窗口尴尬了5分钟。花费了5分钟拿到了现钞。【经典异步】此时柜员喊了1位同事将清点现钞的事情交给这位同事处理交代完事情之后继续回到窗口与我互动。在同事花费15分钟清点完毕后柜员接过现金将现金转交给我。整个过程柜员一直与我互动。花费了15分钟拿到了现钞。【多线程异步】此时柜员喊了1位同事将清点现钞的事情交给这位同事处理交代完事情之后继续回到窗口与我互动。在同事花费清点完毕后由同事直接把现金交给我并且他/她坐下来作为柜员继续与我进行后续互动原来的柜员去后台干别的了。整个过程中始终有一个人与我互动但前半段是柜员A后半段是A的同事B。花费15分钟拿到了现钞。【多线程异步进一步提高效率】此时柜员喊了3位同事由这三位同事负责清点花了7分钟由于提前商量好了他们把清点完的现钞交由他们其中一个人由这位柜员将现钞由窗口递给我。在这7分钟中原来的柜员一直与我互动我收到现钞后由递交给我现钞的那位柜员坐下继续负责与我互动其余柜员去后台干别的去了。整个过程中始终又一位柜员与我互动前半段是柜员A后半段是A的同事B/C/D中提前商量好负责交接的那位。花费7分钟拿到现钞。总而言之异步是为了解决主线程UI冻结而提出的基于协程的架构大部分时候底层是通过多线程实现的。实际工作中我们其实记住一个点就可以很轻易分辨我们到底更需要关注哪种技术的实现I/O密集型操作 —— 异步 吞吐量高【吞吐量一般指相当一段时间内测量出来的系统单位时间处理的任务数或事务数TPS】计算密集型操作 —— 多线程————————————————什么是异步异步是一种任务执行的机制或者说方式它的目的在于解决I/O等待会阻塞线程这个问题最常见的就是GUI线程阻塞造成画面卡顿它的实现依托于硬件底层的IRP(I/O Request Packet)它的本质其实是回调。我可以使用比如 ReadAsync Task.ContinueWith 的组合来实现一个异步实践而更简单的方式是通过微软后续推出 .net4.5 的async-await这套关键词来实践。异步函数 async-await异步函数实际通过 核心类TaskAwaiter 状态机 实现。核心类 TaskAwaiter这个类比较简单每个异步Task都有。我把它看作黑盒不细究只看对外接口IsCompleted 属性表示Task是否完成GetResult() 方法结束异步任务完成的等待UnsafeOnCompleted(Action) 方法设置延续任务使用方法await必须在有async标记的方法内使用。如果async方法内部没有await那它就和同步方法一样执行。如果执行中遇到了await就把需要await执行的Task交给线程池来执行而原来那个线程则退出async方法的方法体去执行外部的后续代码直到await的Task执行完毕返回结果后这个线程会回到方法体await处继续执行。整体流程看上去很像ContinueWith。接下来研究编译器做的事。外部调用层1.首先编译器看到方法有async标记就会为其生成一个实现了IAsyncStateMachine接口的类这个接口意味着它是一个异步状态机。2.async方法自身则会被标记AsyncStateMachine特性意味着这是个异步方法。3.async方法内部则是new了一个异步状态机实例先初始化然后调用它的Start方法来启动状态机。Start方法内部主要是调用了状态机的MoveNext方法。4.最后将指示方法运行状态的builder.Task对象 return 。内部状态机层每个异步状态机都有2个核心字段和一个核心方法builder负责异步相关的操作是方法实现异步执行的核心state状态机的当前状态初始化时赋值为 -1。MoveNext方法状态机切换状态、执行任务、设置延续任务的方法我们直接讲MoveNext方法。第一次MoveNext时我把它分成3块流程1.第一次进入状态机时state!0await之前的代码被包裹到了MoveNext方法体内照常顺序执行。2.await那一行的代码变成了获取任务的awaiterTask.GetAwaiter()。然后对任务的awaiter进行判断​ - 如果awaiter.IsCompleted 为true意味着任务已经执行完了执行第3步。​ - 如果awaiter.IsCompleted 为false意味着任务还未执行完一般初始化完第一次进来都是false那就会做以下操作将state赋值为0将awaiter存到自身字段内供后续使用然后设置延续任务builder.AwaitUnsafeOnCompleted设置完之后会return掉而不执行第3步。延续任务内部怎么设置的比较复杂我觉得不需要理解深入理解为调了awaiter的接口就行了反正最终效果是await的任务完成后再次调用MoveNext转动状态机。3.结束awaiterTaskAwaiter.GetResult()。执行原先await那一行之后的后续代码。执行完成后将state赋值为-2并为builder标记任务成功AsyncTaskMethodBuilder.SetResult()。如果不是第一次MoveNext就会省去1、2步如果await的任务完成就会触发延续任务再次调用MoveNext。但是和第一次进不一样此时因为state0会跳过第1步第2步将state设置为-1之后直接跳入第3步。多层嵌套async-await其实实现也只是多层嵌套异步状态机而已是一样的。一个async中多个await在同一个异步状态机中生成更多的awaiter、更多的state。state的-2完成、-1初始是定好的所以只会从0开始生成而有几个await就有几个awaiter、state。一旦state多起来内部就不再 if-else 了而是 switch-case goto 。上下文接触的主要是SynchronizationContext 同步上下文捕获提供在各种同步模型中传播同步上下文的基本功能。它有一个Post()虚方法Winform、WPF等等会重写它让它被调用时能产生不同的过程但目的都是一个实现使用GUI线程执行Post过去的委托。ExecutionContext 执行上下文流动。在执行委托时恢复另一个线程的状态环境。WPF中使用 async await上面我们都是通过控制台举的例子这是没有任何SynchronizationContext的但是WPFWinform同理就不同了在UI线程中它拥有属于自己的DispatcherSynchronizationContext。这个工作中有体会就是View.xmal.cs文件内写诸如点击事件的async-await延续任务会默认借调GUI线程而非await内分配的任务池线程。。而我可以改成使用SynchronizationContext.Post()来实现类似的效果。怎么用用SynchronizationContext可以实现确定性使用GUI线程来执行委托我的理解只到此。// TODO 光看不行还是得自己实现一套简单的Task封装才行以后参考UniTask。#############################################################################基于任务的异步模式TAP解析##########################################################################################################################################################基础委托回调事件ActionFunc, 线程线程池Task, async/await【语法基础】 委托 Delegate ↓ 【系统内置】 Action(无返回) Func(有返回) ↓ 【使用方式】 回调(参数传递) 事件(安全订阅) 【并发底层】 Thread(系统线程) → ThreadPool(线程池) ↓ 【高级抽象】 Task(统一异步/并发) ↓ 【语法糖】 async / await一、先把 “语法层” 讲清楚最容易混的前 6 个1. 委托 Delegate本质C# 的 “类型安全的函数指针”用来定义一个方法的签名可以把方法当作参数、变量传递是回调、事件、Action、Func 的地基csharp运行// 自定义委托 delegate int Calc(int a, int b);2. Action系统自带的、无返回值委托0~16 个参数返回voidcsharp运行Actionstring act s Console.WriteLine(s);3. Func系统自带的、有返回值委托最后一个泛型是返回值0~16 个参数csharp运行Funcint, int, int add (a,b) a b;4. 回调 Callback用委托 / Action/Func 当参数干完再调用就是延迟执行的方法csharp运行void Work(Action callback) callback();5. 事件 Event被安全限制过的委托外部只能/-不能随便Invoke专门用于订阅 / 通知csharp运行event Action OnCompleted;二、再讲 “并发 / 异步层”后面 4 个6. 线程 Thread操作系统真正创建的线程重量级创建销毁开销大适合长时间运行任务csharp运行new Thread(() { }).Start();7. 线程池 ThreadPool【池化思想】系统预先创建一堆线程复用减少开销适合大量短任务难控制、难等待、无返回值csharp运行ThreadPool.QueueUserWorkItem(_ { });8. Task对异步操作的高级封装两种情况CPU 密集用线程池I/O 密集不占线程IOCP支持等待、返回值、异常、组合csharp运行Task.Run(() 123);9. async / awaitTask 的语法糖让异步代码写得像同步一样csharp运行async Taskint F(){ await Task.Delay(100); return 1; }四、一句话总结最精华委托方法的类型Action/Func系统自带的常用委托回调委托当参数延迟调用事件安全版委托用于订阅Thread操作系统线程重ThreadPool复用线程轻Task异步 / 并发统一封装async/await让 Task 写起来像同步using System; using System.Threading; using System.Threading.Tasks; class Program { static void Main() { // 1 委托 Calc calc Add; Console.WriteLine(委托 calc(1,2)); // 2 Action Actionstring act Console.WriteLine; act(Action无返回); // 3 Func Funcint,int,int func (a,b)a*b; Console.WriteLine(Func func(2,3)); // 4 回调 DoCallback(() Console.WriteLine(回调执行)); // 5 事件 var worker new Worker(); worker.OnWork () Console.WriteLine(事件触发); worker.Run(); // 6 Thread new Thread(()Console.WriteLine(Thread)).Start(); Thread.Sleep(100); // 7 ThreadPool ThreadPool.QueueUserWorkItem(_Console.WriteLine(ThreadPool)); Thread.Sleep(100); // 8 Task Task.Run(()Console.WriteLine(Task)).Wait(); // 9 async/await TestAsync().Wait(); } delegate int Calc(int a,int b); static int Add(int a,int b)ab; static void DoCallback(Action cb)cb(); class Worker{ public event Action OnWork; public void Run()OnWork(); } static async Task TestAsync() { await Task.Delay(100); Console.WriteLine(async/await); } }异步时序流程图一、先看整体结构一句话串完所有plaintext【基础语法】 委托 Delegate ↓ Action / Func系统自带委托 ↓ 回调传方法 / 事件安全订阅 【并发底层】 Thread系统线程→ ThreadPool线程池复用 【现代异步】 Task对线程/IOCP统一封装 ↓ async/await语法糖让异步像同步二、同步代码执行时序对比用plaintext主线程 → 执行代码A → 执行代码B耗时操作 → 执行代码C 【特点】 全程只有一条线B卡住C必须等三、Thread 执行时序多线程plaintext主线程 → 启动新 Thread → 继续往下跑 新线程 → 执行耗时操作 【特点】 两个线程同时跑 但线程创建昂贵多了崩溃四、ThreadPool 执行时序plaintext主线程 → 把任务丢线程池 → 继续往下 线程池线程 → 有空线程就执行任务 【特点】 线程复用省资源 但无返回值、难等待、难抓异常五、Task 执行时序Task.Run 版本plaintext主线程 → Task.Run(耗时任务) → 继续执行 线程池线程 → 执行任务 → 任务完成 → 标记 Task 为完成 【特点】 可以 Wait、可以拿 Result、可以抓异常 但 Wait/Result 会阻塞主线程六、async/await 真正异步时序最重要plaintext主线程遇到 async Task Test() { CodeA(); await DoIOAsync(); ← 关键点 CodeB(); } 执行流程 1. 主线程运行 CodeA 2. 碰到 await 3. **主线程直接返回调用者去干别的** 比如响应用户点击、刷新界面 4. 底层系统IOCP等待 I/O 完成 **这期间没有任何线程在等待** 5. I/O 完成后 6. 线程池线程或主线程回来执行 CodeB文字版时序图plaintext主线程 CodeA → await → 退出方法 → 去干别的 I/O 操作网络/文件/数据库 ...等待硬件/网络... 不占线程 I/O 完成后 线程池线程 → 执行 CodeB核心结论await 不会卡住线程I/O 异步期间没有线程在等待这才是高并发的关键七、一张总图从委托到 async/await 进化史plaintext1. 委托 ↓ 用来传方法 2. 回调 / Action / Func / 事件 ↓ 想并发 3. Thread太重 ↓ 想省资源 4. ThreadPool难用 ↓ 想统一管理 5. Task支持等待、返回、异常 ↓ 想写得简单 6. async/await最终形态八、最容易混淆的 3 个问题一句话答案Task 一定开线程吗不一定。I/O 异步HttpClient、FileStream不开线程只靠系统 IOCP。await 时线程在干嘛解放了去干别的了。没有线程空转。Task.Run 和 真正异步的区别Task.Run用线程池模拟异步CPU 密集用await HttpClient.GetStringAsync真异步不占线程I/O 密集用#############################################################################参考https://www.cnblogs.com/xiaoxiaotank/p/14303803.htmlhttps://www.cnblogs.com/xiaoxiaotank/p/13666913.htmlhttps://codingcodingk.top/2022/01/14/Tech/CSharp/CLR-Via-CSharp/cp7/Posts | Kit Laus Blog异步的原理是什么C# 如何基于状态机实现异步 | Kit Laus Blog

更多文章