C# 8.0 寶藏好物 Async streams
阿新 • • 發佈:2021-03-30
> 之前寫《.NET gRPC 核心功能初體驗》,利用gRPC雙向流做了一個打乒乓的Demo,儲存訊息的物件是`IAsyncEnumerable`,這個`非同步可列舉泛型介面`支撐了gRPC的實時流式通訊。
本文我將回顧分享
- foreach/yield return/async await語法糖的本質
- 如何使用非同步流
- 消費非同步流時 附加探索
#### foreach/ yield return/async await的本質
.NET誕生之初,就通過IEnumerable、IEnumerator提供迭代能力,
前者代表具備可列舉的性質,後者代表可被列舉的方式。
(看你骨骼驚奇,再送你一本《2021年了,`IEnumerable`、`IEnumerator`介面還傻傻分不清楚?》)
如果你真的使用強型別IEnumerable/IEnumerator來產生/消費可列舉型別,會發現要寫很多瑣碎程式碼。
C#推出的`yield return`迭代器語法糖,簡化了產生可列舉型別的編寫過程。(編譯器將yield return轉換為狀態機程式碼來實現IEnumerable,IEnumerator)
> yield 關鍵字可以執行狀態迭代,並逐個返回列舉元素,在返回資料時,無需建立臨時集合來儲存資料。
C#`foreach`語法糖,簡化了消費可列舉型別的編寫過程。(編譯器將foreach抓換為強型別的方法/屬性呼叫)
```
IEnumerable src = ...;
IEnumerator e = src.GetEnumerator();
try
{
while (e.MoveNext()) Use(e.Current);
}
finally { if (e != null) e.Dispose(); }
```
.NET Framework4引入Task,.NET Framework 4.5/C#5.0引入了`await/async`非同步程式設計語法糖,簡化了非同步程式設計的程式設計過程。(編譯器將await/async語法糖轉換為狀態機,產生Task並在內部回撥)
☺️以上也看出微軟為幫助我們更快速優雅地編寫程式碼,給了很多糖,編譯器做了很多事情。
C#提供了迭代、非同步的快捷方式,能否將兩者結合?
兩者結合的效果就是: 希望在資料就緒時,接受並處理資料,但不會以阻塞CPU的sing是等待,這在lot流式資料中很常見,
#### 非同步迭代
有一隻爬蟲要通過列表頁上的連結,抓取連結背後的html內容並顯示。
![](https://files.mdnice.com/tmp/4236/b0d1a401-ba7e-4eb3-ad09-48a95472cc9f.png)
這是一個[相互獨立的長耗時行為的集合(假設分別耗時5,4,3,2,1s)],
我們使用C#8.0非同步可列舉型別IAsyncEnumerable,非同步產生/消費列舉元素。
> 與同步版本IEmunerable類似,IAsyncEnumerable也有對應的IAsyncEnumerator迭代器,迭代器的實現過程決定了消費的順序。
#### C#8.0 Asynchronous streams
C#8.0中一個重要的特性是非同步流(async stream), 可以輕鬆建立和消費非同步列舉。
返回非同步流的方法特徵:
- 以`async`修飾符宣告
- 返回`IAsyncEnumerable`物件
- 方法包含`yield return`語句,用來非同步持續返回元素
```
static async Task Main(string[] args)
{
Console.WriteLine(DateTime.Now + $"\tThreadId:{Thread.CurrentThread.ManagedThreadId}\r\n");
await foreach (var html in FetchAllHtml())
{
Console.WriteLine(DateTime.Now + $"\tThreadId:{Thread.CurrentThread.ManagedThreadId}\t" + $"\toutput:{html}");
}
Console.WriteLine("\r\n" + DateTime.Now + $"\tThreadId:{Thread.CurrentThread.ManagedThreadId}\t");
Console.ReadKey();
}
static async IAsyncEnumerable FetchAllHtml()
{
for (int i = 5; i >= 1; i--)
{
var html = await Task.Delay(i* 1000).ContinueWith((t,i)=> $"html{i}",i); // 模擬長耗時
yield return html;
}
}
```
for迴圈結合yield關鍵字,決定了IAsyncEnymerator的實現;
以上程式碼將使得`await foreach消費非同步列舉`時, 採用與for迴圈一樣的順序,也就是**產生非同步任務的先後順序**。
![](https://files.mdnice.com/user/4236/1efc88ad-8490-4b9e-8369-6ad1731f8d9d.png)
以上不會等待15s然後一股腦丟擲所有資料,而是根據列舉for迴圈,一次就緒,依次顯示,總耗時還是15s,只不過每一步都是非同步的。
#### 附加思考:實現一個更有意思的迭代器
☺️ 但是我內心想,能不能按照**完成非同步任務的順序,先完成先消費**,這難道不是人之常情,互動體驗應該更好。
```
static async IAsyncEnumerable FetchAllHtml()
{
var tasklist= new List>();
for (int i = 5; i >= 1; i--)
{
var t= Task.Delay(i* 1000).ContinueWith((t,i)=>$"html{i}",i); // 模擬長耗時任務
tasklist.Add(t);
}
while(tasklist.Any())
{
var tFinlish = await Task.WhenAny(tasklist);
tasklist.Remove(tFinlish);
yield return await tFinlish;
}
}
```
上面我先構造了可等待的任務列表,通過Task.WhenAny()按照任務完成的順序 返回迭代。
![](https://files.mdnice.com/user/4236/a18f3a7f-4c73-44b3-ad7a-a0906b2eaa23.png)
以上總耗時取決於 耗時最長的那個非同步任務5s.
---
.NETCore 3.1 已經可以在webapi中使用非同步流,意味著我們可將流式資料返回到HTTP響應。
前端也已經有試驗性的`Streams API`可以對接消費流式資料。
傳送門: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
瀏覽器相容列表: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API#browser_compatibility
對於web應用,這著實能提高 可互動性:
想象之前含多個長耗時行為的列表資料,現在不必等待所有資料,,配以loading,誰家完成誰載入,效果槓槓。