C#8.0: 在 LINQ 中支持非同步的 IAsyncEnumerable

来源:https://www.cnblogs.com/yanxiaodi/archive/2019/07/12/Support-IAsyncEnumerable-with-LINQ.html
-Advertisement-
Play Games

C# 8.0中,提供了一種新的IAsyncEnumerable<T>介面,在對集合進行迭代時,支持非同步操作。比如在讀取文本中的多行字元串時,如果讀取每行字元串的時候使用同步方法,那麼會導致線程堵塞。IAsyncEnumerable<T>可以解決這種情況,在迭代的時候支持使用非同步方法。也就是說,之前我 ...


C# 8.0中,提供了一種新的IAsyncEnumerable<T>介面,在對集合進行迭代時,支持非同步操作。比如在讀取文本中的多行字元串時,如果讀取每行字元串的時候使用同步方法,那麼會導致線程堵塞。IAsyncEnumerable<T>可以解決這種情況,在迭代的時候支持使用非同步方法。也就是說,之前我們使用foreach來對IEnumerable進行迭代,現在可以使用await foreach來對IAsyncEnumerable<T>進行迭代,每個項都是可等待的。這種新的介面稱為async-streams,將會隨.NET Core 3發佈。我們來看一下如何在LINQ中實現非同步的迭代。

使用常規的IEnumerable<T>

首先我們創建一個新的Console項目,基於.NET Core 3

namespace AsyncLinqDemo
{
   class Program
  {
       static void Main(string[] args)
      {
           Console.WriteLine("Input the file path:");
           var file = Console.ReadLine();
           var lines = ReadAllLines(file);
           foreach (var line in lines)
          {
               Console.WriteLine(line);
          }
      }

       static IEnumerable<string> ReadAllLines(string file)
      {
           using (var fs = File.OpenRead(file))
          {
               using (var sr = new StreamReader(fs))
              {
                   while (true)
                  {
                       string line = sr.ReadLine();
                       if(line == null)
                      {
                           break;
                      }
                       yield return line;
                  }
              }
          }
      }
  }
}

 

這是一個很簡單的Console程式,實現了一個簡單的返回類型為IEnumerable<string>ReadAllLines(string file)方法,從文本文件中逐行讀取文本,並逐行輸出。如果文本內容較少的話,沒什麼問題。但如果我們使用過aync/await,就會瞭解,在IO操作如讀取或寫入文件的時候,最好使用非同步方法以避免線程阻塞。讓我們來改進一下。

使用非同步的IAsyncEnumerable<T>

可以優化的是下麵這句:

string line = sr.ReadLine();

 

對於IO操作,最好使用非同步方式。這裡可使用相應的非同步方法:

string line = await sr.ReadLineAsync();

 

我們說“非同步是傳染的”,如果這裡使用非同步,那麼相應的該方法的返回值也要使用非同步,所以需要將返回值改為static async Task<IEnumerable<string>>,但這樣會得到一個錯誤:

ErrorCS1624The body of 'Program.ReadAllLines(string)' cannot be an iterator block because 'Task<IEnumerable<string>>' is not an iterator interface typeAsyncLinqDemoC:\Source\Workspaces\Console\AsyncLinqDemo\AsyncLinqDemo\Program.cs23Active

 

因為Task<IEnumerable<string>>並不是一個可以迭代的介面類型,所以我們無法在方法內部使用yield關鍵字。解決問題的辦法是使用新的IAsyncEnumerable介面:

static async IAsyncEnumerable<string> ReadAllLines(string file)
{
   using (var fs = File.OpenRead(file))
  {
       using (var sr = new StreamReader(fs))
      {
           while (true)
          {
               string line = await sr.ReadLineAsync();
               if(line == null)
              {
                   break;
              }
               yield return line;
          }

      }
  }
}

 

F12查看該介面的定義:

namespace System.Collections.Generic
{
   public interface IAsyncEnumerable<out T>
  {
       IAsyncEnumerator<T> GetAsyncEnumerator(CancellationTokencancellationToken = default);
  }
}

 

這是一個非同步的迭代器,並提供了CancellationToken。再按F12查看IAsyncEnumerator<T>的定義,可發現裡面是這樣的:

namespace System.Collections.Generic
{
   public interface IAsyncEnumerator<out T> : IAsyncDisposable
  {
       T Current { get; }
       ValueTask<bool> MoveNextAsync();
  }
}

 

這裡MoveNextAsync()方法實際是返回了一個結果類型為boolTask,每次迭代都是可等待的,從而實現了迭代器的非同步。

使用await foreach消費IAsyncEnumerable<T>

當我們做了以上改動之後,ReadAllLines()方法返回的是一個支持非同步的IAsyncEnumerable,那麼在使用的時候,也不能簡單的使用foreach了。修改Main方法如下:

static async Task Main(string[] args)
{
   Console.WriteLine("Input the file path:");
   var file = Console.ReadLine();
   var lines = ReadAllLines(file);
   await foreach (var line in lines)
  {
       Console.WriteLine(line);
  }
}

 

首先在foreach之前添加await關鍵字,還要需要將Main方法由void改為async Task。這樣整個程式都是非同步執行了,不會再導致堵塞了。這個例子只是一個簡單的demo,是否使用非同步並不會感覺到明顯的區別。如果在迭代內部需要比較重的操作,如從網路獲取大量數據或讀取大量磁碟文件,非同步的優勢還是會比較明顯的。

使用LINQ消費IAsyncEnumerable<T>

使用LINQ來操作集合是常用的功能。如果使用IEnumberable,在Main方法中可以做如下改動:

var lines = ReadAllLines(file);
var res = from line in lines where line.StartsWith("ERROR: ") selectline.Substring("ERROR: ".Length);
foreach (var line in res)
{
   Console.WriteLine(line);
}

 

或:

var res = lines.Where(x => x.StartsWith("ERROR: ")).Select(x => x.Substring("ERROR: ".Length));

 

如果使用了新的IAsyncEnumerable,你會發現無法使用Where等操作符了:

ErrorCS1936Could not find an implementation of the query pattern for source type 'IAsyncEnumerable<string>'. 'Where' not found.AsyncLinqDemoC:\Source\Workspaces\Console\AsyncLinqDemo\AsyncLinqDemo\Program.cs16Active

 

目前LINQ還沒有提供對IAsyncEnumerable的原生支持,不過微軟提供了一個Nuget包來實現此功能。在項目中打開Nuget Package Manger搜索安裝System.Linq.Async,註意該包目前還是預覽版,所以要勾選include prerelease才能看到。安裝該Nuget包後,Linq查詢語句中的錯誤就消失了。

System.Linq.Async這個包中,對每個同步的LINQ方法都做了相應的擴展。所以基本上代碼無需什麼改動即可正常編譯。

對於LINQ中的條件語句,也可以使用WhereAwait()方法來支持await

public static IAsyncEnumerable<TSource> WhereAwait<TSource>(thisIAsyncEnumerable<TSource> source, Func<TSource, int, ValueTask<bool>>predicate);

 

如需要在條件語句中進行IO或網路請求等非同步操作,可以這樣用:

var res = lines.WhereAwait(async x => await DoSomeHeavyOperationsAsync(x));

 

DoSomeHeavyOperationsAsync方法的簽名如下:

private static ValueTask<bool> DoSomeHeavyOperationsAsync(string x)
{
   //Do some works...
}

 

小結

通過以上的示例,我們簡要瞭解瞭如何使用IAsyncEnumerable介面以及如何在LINQ中實現非同步查詢。在使用該介面時,我們需要創建一個自定義方法返回IAsyncEnumerable<T>來代替IEnumberable<T>,這個方法可稱為async-iterator方法,需要註意以下幾點:

  • 該方法應該被聲明為async

  • 返回IAsyncEnumerable<T>

  • 同時使用awaityield。如await foreachyield returnyield break等。

例如:

async IAsyncEnumerable<int> GetValuesFromServer()
{
   while (true)
  {
       IEnumerable<int> batch = await GetNextBatch();
       if (batch == null) yield break;

       foreach (int item in batch)
      {
           yield return item;
      }
  }
}

 

此外還有一些限制:

  • 無法在tryfinally塊中使用任何形式的yield語句。

  • 無法在包含任何catch語句的try語句中使用yield return語句。

     

期待.NET Core 3的正式發佈!

 

瞭解紐西蘭IT行業真實碼農生活

請長按上方二維碼關註“程式員在紐西蘭”


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這幾天看書的時候看到一個演算法,叫粒子群演算法,這個演算法挺有意思的,下麵說說我個人的理解: 粒子群演算法(PSO)是一種進化演算法,是一種求得近似最優解的演算法,這種演算法的時間複雜度可能會達到O(n!),得到的結果不一定是最優解,往往已經很接近最優解了。最早是Kenny 和 Eberhart於1995年提出的 ...
  • 一、python3中安裝PyMySQL模塊 命令安裝: 或者 2、使用在pyCharm中安裝PyMySQL模塊 二、PyMysql對象說明 1、Connection對象 用於建立與資料庫的連接 創建對象:調用connect()方法 參數host:連接的mysql主機,如果本機是'localhost' ...
  • 一、線程池 Sun在Java5中,對Java線程的類庫做了大量的擴展,其中線程池就是Java5的新特征之一,除了線程池之外,還有很多多線程相關的內容,為多線程的編程帶來了極大便利。為了編寫高效穩定可靠的多線程程式,線程部分的新增內容顯得尤為重要。 有關Java5線程新特征的內容全部在java.uti ...
  • 1.介面編程 1.1背景 隨著互聯網的發展, 尤其是移動互聯為代表的Web3.0時代. 客戶端層出不窮, 以APP、微信、PC瀏覽器為代表, 服務端業務邏輯是基本一致的。那麼有沒有一種方式可以做到”服務端一次編寫, 客戶端隨時接入”呢? 1.2介面編程 API(Application Program ...
  • hibernate實體的狀態 實體Entity有三種狀態,瞬時狀態,持久狀態,脫管狀態 瞬時狀態:transient,session 沒有緩存,資料庫也沒有記錄,oid沒有值 持久狀態:persistent,session有緩存,資料庫也有記錄,oid有值 脫管狀態:detached,session ...
  • Python簡介 Python是一種電腦程式設計語言。是一種面向對象的動態類型語言,最初被設計用於編寫自動化腳本(shell),隨著版本的不斷更新和語言新 功能的添加,越來越多被用於獨立的、大型項目的開發。 Python是一門入門非常簡單的編程語言,也是目前很受歡迎的編程語言,在人工智慧、網路爬蟲 ...
  • 測試函數 學習測試,得有測試的代碼。下麵是一個簡單的函數: 為核實get_formatted_name()像期望的那樣工作,編寫一個使用這個函數的程式: 運行: 從輸出可知,合併得到的姓名正確無誤。現在假設要修改get_formatted_name(),使其還能夠處理中間名。確保不破化這個函數處理只 ...
  • aspnetcore 實現簡單的偽靜態化 Intro 在我的活動室預約項目中,有一個公告模塊,類似於新聞發佈,個人感覺像新聞這種網頁基本就是發佈的時候編輯一次之後就再也不會改了,最適合靜態化了, 靜態化之後用戶請求的就是靜態文件基本不再需要伺服器端查詢資料庫甚至伺服器端渲染,可以一定程度上提升伺服器 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...