<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          .NET 異步詳解

          共 9529字,需瀏覽 20分鐘

           ·

          2020-10-13 02:40

          前言

          博客園中有很多關(guān)于 .NET?async/await?的介紹,但是很遺憾,很少有正確的,甚至說大多都是“從現(xiàn)象編原理”都不過分。

          最典型的比如通過前后線程 ID 來推斷其工作方式、在?async?方法中用?Thread.Sleep?來解釋?Task?機制而導(dǎo)出多線程模型的結(jié)論、在?Task.Run?中包含 IO bound 任務(wù)來推出這是開了一個多線程在執(zhí)行任務(wù)的結(jié)論等等。

          看上去似乎可以解釋的通,可是很遺憾,無論是從原理還是結(jié)論上看都是錯誤的。

          要了解 .NET 中的?async/await?機制,首先需要有操作系統(tǒng)原理的基礎(chǔ),否則的話是很難理解清楚的,如果沒有這些基礎(chǔ)而試圖向他人解釋,大多也只是基于現(xiàn)象得到的錯誤猜想。

          初看異步

          說到異步大家應(yīng)該都很熟悉了,2012 年 C# 5 引入了新的異步機制:Task,并且還有兩個新的關(guān)鍵字?await?和?async,這已經(jīng)不是什么新鮮事了,而且如今這個異步機制已經(jīng)被各大語言借鑒,如 JavaScript、TypeScript、Rust、C++ 等等。

          下面給出一個簡單的對照:

          語言調(diào)度單位關(guān)鍵字/方法
          C#Task<>ValueTask<>asyncawait
          C++std::future<>co_await
          Ruststd::future::Future<>.await
          JavaScript、TypeScriptPromise<>asyncawait

          當(dāng)然,這里這并不是本文的重點,只是提一下,方便大家在有其他語言經(jīng)驗的情況下(如果有),可以認識到 C# 中?Task?和?async/await?究竟是一個和什么可以相提并論的東西。

          多線程編程

          在該異步編程模型誕生之前,多線程編程模型是很多人所熟知的。一般來說,開發(fā)者會使用?Threadstd::thread?之類的東西作為線程的調(diào)度單位來進行多線程開發(fā),每一個這樣的結(jié)構(gòu)表示一個對等線程,線程之間采用互斥或者信號量等方式進行同步。

          多線程對于科學(xué)計算速度提升等方面效果顯著,但是對于 IO 負荷的任務(wù),例如從讀取文件或者 TCP 流,大多數(shù)方案只是分配一個線程進行讀取,讀取過程中阻塞該線程:

          Copy
          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?函數(shù)在接收客戶端之后即分配了一個新的用戶線程用于處理該客戶端,從客戶端接收數(shù)據(jù)。client.Read()?執(zhí)行后,該線程即被阻塞,即使阻塞期間該線程沒有任何的操作,該用戶線程也不會被釋放,并被操作系統(tǒng)不斷輪轉(zhuǎn)調(diào)度,這顯然浪費了資源。

          另外,如果線程數(shù)量多起來,頻繁在不同線程之間輪轉(zhuǎn)切換上下文,線程的上下文也不小,會浪費掉大量的性能。

          異步編程

          因此對于此工作內(nèi)容(IO),我們在 Linux 上有了 epoll/io_uring 技術(shù),在 Windows 上有了 IOCP 技術(shù)用以實現(xiàn)異步 IO 操作。

          (這里插句題外話,吐槽一句,Linux 終于知道從 Windows 抄作業(yè)了。先前的 epoll 對比 IOCP 簡直不能打,被 IOCP 全面打壓,io_uring 出來了才好不容易能追上 IOCP,不過 IOCP 從 Windows Vista 時代開始每一代都有很大的優(yōu)化,io_uring 能不能追得上還有待商榷)

          這類 API 有一個共同的特性就是,在操作 IO 的時候,調(diào)用方控制權(quán)被讓出,等待 IO 操作完成之后恢復(fù)先前的上下文,重新被調(diào)度繼續(xù)運行。

          所以表現(xiàn)就是這樣的:

          假設(shè)我現(xiàn)在需要從某設(shè)備中讀取 1024 個字節(jié)長度的數(shù)據(jù),于是我們將緩沖區(qū)的地址和內(nèi)容長度等信息封裝好傳遞給操作系統(tǒng)之后我們就不管了,讀取什么的讓操作系統(tǒng)去做就好了。

          操作系統(tǒng)在內(nèi)核態(tài)下利用 DMA 等方式將數(shù)據(jù)讀取了 1024 個字節(jié)并寫入到我們先前的 buffer 地址下,然后切換到用戶態(tài)將從我們先前讓出控制權(quán)的位置,對其進行調(diào)度使其繼續(xù)執(zhí)行。

          你可以發(fā)現(xiàn)這么一來,在讀取數(shù)據(jù)期間就沒有任何的線程被阻塞,也不存在被頻繁調(diào)度和切換上下文的情況,只有當(dāng) IO 操作完成之后才會被重新調(diào)度并恢復(fù)先前讓出控制權(quán)時的上下文,使得后面的代碼繼續(xù)執(zhí)行。

          當(dāng)然,這里說的是操作系統(tǒng)的異步 IO 實現(xiàn)方式,以便于讀者對異步這個行為本身進行理解,和 .NET 中的異步還是有區(qū)別,Task?本身和操作系統(tǒng)也沒什么關(guān)系。

          Task (ValueTask)

          說了這么久還是沒有解釋?Task?到底是個什么東西,從上面的分析就可以得出,Task?其實就是一個所謂的調(diào)度單位,每個異步任務(wù)被封裝為一個?Task?在 CLR 中被調(diào)度,而?Task?本身會運行在 CLR 中的預(yù)先分配好的線程池中。

          總有很多人因為?Task?借助線程池執(zhí)行而把?Task?歸結(jié)為多線程模型,這是完全錯誤的。

          這個時候有人跳出來了,說:你看下面這個代碼

          Copy
          static async Task Main()
          {
          while (true)
          {
          Console.WriteLine(Environment.CurrentManagedThreadId);
          await Task.Delay(1000);
          }
          }

          輸出的線程 ID 不一樣欸,你騙人,這明明就是多線程!對于這種言論,我也只能說這些人從原理上理解的就是錯誤的。

          當(dāng)代碼執(zhí)行到?await?的時候,此時當(dāng)前的控制權(quán)就已經(jīng)被讓出了,當(dāng)前線程并沒有在阻塞地等待延時結(jié)束;待?Task.Delay()?完畢后,CLR 從線程池當(dāng)中挑起了一個先前分配好的已有的但是空閑的線程,將讓出控制權(quán)前的上下文信息恢復(fù),使得該線程恰好可以從先前讓出的位置繼續(xù)執(zhí)行下去。這個時候,可能挑到了先前讓出前所在的那個線程,導(dǎo)致前后線程 ID 一致;也有可能挑到了另外一個和之前不一樣的線程執(zhí)行下面的代碼,使得前后的線程 ID 不一致。在此過程中并沒有任何的新線程被分配了出去。

          當(dāng)然,在 WPF 等地方,因為利用了?SynchronizationContext?對調(diào)度行為進行了控制,所以可以得到和上述不同的結(jié)論,和這個相關(guān)的還有?.ConfigureAwait()?的用法,但是這里不是本文重點,因此就不做展開。

          在 .NET 中由于采用 stackless 的做法,這里需要用到 CPS 變換,大概是這么個流程:

          Copy
          using System;
          using System.Threading.Tasks;

          public class C
          {
          public async Task M()
          {
          var a = 1;
          await Task.Delay(1000);
          Console.WriteLine(a);
          }
          }

          編譯后:

          Copy
          public class C
          {
          [StructLayout(LayoutKind.Auto)]
          [CompilerGenerated]
          private struct d__0 : IAsyncStateMachine
          {
          public int <>1__state;

          public AsyncTaskMethodBuilder <>t__builder;

          private int 5__2;

          private TaskAwaiter <>u__1;

          private void MoveNext()
          {
          int num = <>1__state;
          try
          {
          TaskAwaiter awaiter;
          if (num != 0)
          {
          5__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(
          5__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(d__0))]
          public Task M()
          {
          d__0 stateMachine = default(d__0);
          stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
          stateMachine.<>1__state = -1;
          stateMachine.<>t__builder.Start(ref stateMachine);
          return stateMachine.<>t__builder.Task;
          }
          }

          可以看到,原來的變量?a?被塞到了?5__2?里面去(相當(dāng)于備份上下文),Task 狀態(tài)的轉(zhuǎn)換后也是靠著調(diào)用?MoveNext(相當(dāng)于狀態(tài)轉(zhuǎn)換后被重新調(diào)度)來接著驅(qū)動代碼執(zhí)行的,里面的?num?就表示當(dāng)前的狀態(tài),num?如果為 0 表示 Task 完成了,于是接著執(zhí)行下面的代碼?Console.WriteLine(5__2);

          但是上面和經(jīng)典的多線程編程的那一套一樣嗎?不一樣。

          至于?ValueTask?是個什么玩意,官方發(fā)現(xiàn),Task?由于本身是一個?class,在運行時如果頻繁反復(fù)的分配和回收會給 GC 造成不小的壓力,因此出了一個?ValueTask,這個東西是?struct,分配在棧上,這樣的話就不會給 GC 造成壓力了,減輕了開銷。不過也正因為?ValueTask?是會在棧上分配的值類型結(jié)構(gòu),因此提供的功能也不如?Task?全面。

          Task.Run

          由于 .NET 是允許有多個線程的,因此也提供了?Task.Run?這個方法,允許我們將 CPU bound 的任務(wù)放在上述的線程池之中的某個線程上執(zhí)行,并且允許我們將該負載作為一個?Task?進行管理,僅在這一點才和多線程的采用線程池的編程比較像。

          對于瀏覽器環(huán)境(v8),這個時候是完全沒有多線程這一說的,因此你開的新的?Promise?其實是后面利用事件循環(huán)機制,將該微任務(wù)以異步的方式執(zhí)行。

          想一想在 JavaScript 中,Promise?是怎么用的:

          let p = new Promise((resolve, reject) => {
          // do something
          let success = true;
          let result = 123456;

          if (success) {
          resolve(result);
          }
          else {
          reject("failed");
          }
          })

          然后調(diào)用:

          let r = await p;
          console.log(r); // 輸出 123456

          你只需要把這一套背后的驅(qū)動器:事件循環(huán)隊列,替換成 CLR 的線程池,就差不多是 .NET 的?Task?相對 JavaScript 的?Promise?的工作方式了。

          如果你把 CLR 線程池線程數(shù)量設(shè)置為 1,那就和 JavaScript 這套幾乎差不多了(雖然實現(xiàn)上還是有差異)。

          這時有人要問了:“我在 Task.Run 里面套了好幾層 Task.Run,可是為什么層數(shù)深了之后里面的不執(zhí)行了呢?” 這是因為上面所說的線程池被耗盡了,后面的?Task?還在排著隊等待被調(diào)度。

          自己封裝異步邏輯

          了解了上面的東西之后,相信對 .NET 中的異步機制應(yīng)該理解得差不多了,可以看出來這一套是名副其實的 coroutine,并且在實現(xiàn)上是 stackless 的。至于有的人說的什么狀態(tài)機什么的,只是實現(xiàn)過程中利用的手段而已,并不是什么重要的東西。

          那我們要怎么樣使用?Task?來編寫我們自己的異步代碼呢?

          事件驅(qū)動其實也可以算是一種異步模型,例如以下情景:

          A?函數(shù)調(diào)用?B?函數(shù),調(diào)用發(fā)起后就直接返回不管了(BeginInvoke),B?函數(shù)執(zhí)行完成后觸發(fā)事件執(zhí)行?C?函數(shù)。

          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");
          }

          那么我們現(xiàn)在想要做一件事,就是把上面的事件驅(qū)動改造為利用?async/await?的異步編程模型,改造后的代碼就是簡單的:

          async Task A()
          {
          Console.WriteLine("begin");
          await B();
          Console.WriteLine("end");
          }

          Task B()
          {
          Console.WriteLine("running");
          return Task.CompletedTask;
          }

          你可以看到,原本?C?函數(shù)的內(nèi)容被放到了?A?調(diào)用?B?的下面,為什么呢?其實很簡單,因為這里?await B();?這一行以后的內(nèi)容,本身就可以理解為?B?函數(shù)的回調(diào)了,只不過在內(nèi)部實現(xiàn)上,不是直接從?B?進行調(diào)用的回調(diào),而是?A?先讓出控制權(quán),B?執(zhí)行完成后,CLR 切換上下文,將?A?調(diào)度回來繼續(xù)執(zhí)行剩下的代碼。

          如果事件相關(guān)的代碼已經(jīng)確定不可改動(即不能改動 B 函數(shù)),我們想將其封裝為異步調(diào)用的模式,那只需要利用?TaskCompletionSource?即可:

          private event Action CompletedEvent;

          async Task A()
          {
          // 因為 TaskCompletionSource 要求必須有一個泛型參數(shù)
          // 因此就隨便指定了一個 bool
          // 本例中其實是不需要這樣的一個結(jié)果的
          // 需要注意的是從 .NET 5 開始
          // TaskCompletionSource 不再強制需要泛型參數(shù)
          var tsc = new TaskCompletionSource<bool>();
          // 隨便寫一個結(jié)果作為 Task 的結(jié)果
          CompletedEvent += () => tsc.SetResult(false);

          Console.WriteLine("begin");
          ((Action)B).BeginInvoke();
          await tsc.Task;
          Console.WriteLine("end");
          }

          void B()
          {
          Console.WriteLine("running");
          CompletedEvent?.Invoke();
          }

          順便提一句,這個?TaskCompletionSource?其實和 JavaScript 中的?Promise?更像。SetResult()?方法對應(yīng)?resolve()SetException()?方法對應(yīng)?reject()。.NET 比 JavaScript 還多了一個取消狀態(tài),因此還可以?SetCancelled()?表示任務(wù)被取消了。

          同步方式調(diào)用異步代碼

          說句真的,一般能有這個需求,都說明你的代碼寫的有問題,但是如果你無論如何都想以阻塞的方式去等待一個異步任務(wù)完成的話:

          Task t = ...
          t.GetAwaiter().GetResult();

          祝你好運,這相當(dāng)于,t?中的異步任務(wù)開始執(zhí)行后,你將當(dāng)前線程阻塞,然后等到?t?完成之后再喚醒,可以說是:毫無意義,而且很有可能因為代碼編寫不當(dāng)而導(dǎo)致死鎖的發(fā)生。

          void async 是什么?

          最后有人會問了,函數(shù)可以寫?async Task Foo(),還可以寫?async void Bar(),這有什么區(qū)別呢?

          對于上述代碼,我們一般調(diào)用的時候,分別這么寫:

          await Foo();
          Bar();

          可以發(fā)現(xiàn),誒這個?Bar?函數(shù)不需要?await?誒。為什么呢?

          其實這和用以下方式調(diào)用?Foo?是一樣的:

          _ = Foo();

          換句話說就是調(diào)用后瞬間就直接拋掉不管了,不過這樣你也就沒法知道這個異步任務(wù)的狀態(tài)和結(jié)果了。

          await 必須配合 Task/ValueTask 才能用嗎?

          當(dāng)然不是。

          在 C# 中只要你的類中包含?GetAwaiter()?方法和?bool IsCompleted?屬性,并且?GetAwaiter()?返回的東西包含一個?GetResult()?方法、一個?bool IsCompleted?屬性和實現(xiàn)了?INotifyCompletion,那么這個類的對象就是可以?await?的。

          public class MyTask<T>
          {
          public MyAwaiter GetAwaiter()
          {
          return new MyAwaiter();
          }
          }

          public class MyAwaiter<T> : 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 MyTask<int>();
          await obj;
          }
          }

          結(jié)語

          本文至此就結(jié)束了,感興趣的小伙伴可以多多學(xué)習(xí)一下操作系統(tǒng)原理,對 CLR 感興趣也可以去研究其源代碼:https://github.com/dotnet/runtime?。

          .NET 的異步和線程密不可分,但是和多線程編程方式和思想是有本質(zhì)不同的,也希望大家不要將異步和多線程混淆了,而這有聯(lián)系也有區(qū)別。

          從現(xiàn)象猜測本質(zhì)是大忌,可能解釋的通但是終究只是偶然現(xiàn)象,而且從原理上看也是完全錯誤的,甚至官方的實現(xiàn)代碼稍微變一下可能立馬就無法解釋的通了。

          總之,通過本文希望大家能對異步和 .NET 中的異步有一個更清晰的理解。

          感謝閱讀。


          瀏覽 61
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  加勒比久操视频 | 高清无码视频免费版本在线观看 | 大香蕉大香蕉最新视频97 | 激情婷婷五月天 | 男女射精视频 |