介紹 TPL 資料流庫
TPL Dataflow 庫的歷史與 TPL 本身一樣長。 它於 2010 年 .NET Framework 4.0 達到其 RTM 里程碑後釋出。 資料流庫的成員是
System.Threading.Tasks.Dataflow
名稱空間。 資料流庫旨在構建 TPL 中提供的並行程式設計基礎知識,並進行擴充套件以解決資料流場景(該庫的名稱由此而來)。 資料流庫由稱為塊的基礎類組成。 每個資料流塊負責整個流中的特定操作或步驟。
資料流庫由三種基本型別的塊組成:
-
源塊(Source blocks):這些塊實現
ISourceBlock<TOutput>
介面。 源塊可以從您定義的工作流程中讀取資料。 -
目標塊(Target blocks):此類塊實現
ITargetBlock<TInput>
介面,並且是資料接收器。 -
傳播器塊(Propagator blocks:):這些塊既充當源又充當目標。 它們實現
IPropagatorBlock<TInput, TOutput>
介面。 應用程式可以從這些塊中讀取資料並向其中寫入資料。
當您連線多個資料流塊來建立工作流時,生成的系統稱為資料流管道。 您可以使用 ISourceBlock<TOutput>.LinkTo
方法將源塊連線到目標塊。 這是傳播器塊可以安裝在管道中間的地方。 它們可以充當工作流程中連結的源和目標。 如果來自源塊的訊息可以由多個目標處理,您可以新增過濾來檢查源提供的物件的屬性,以確定哪個目標或傳播器塊應接收該物件。
在資料流塊之間傳遞的物件通常稱為訊息。 您可以將資料流管道視為網路或訊息傳遞系統。 流經網路的資料單位是訊息。 每個塊負責以某種方式讀取、寫入或轉換每條訊息。
要將訊息傳送到目標塊,可以使用 Post
方法同步傳送,也可以使用 SendAsync
方法非同步傳送。 在源塊中,可以使用以下命令接收訊息
Receive
、TryReceive
和 ReceiveAsync
方法。 Receive
和 TryReceive
方法都是同步的。 Choose
方法將監視多個源塊的資料,並從第一個源返回訊息以提供資料。
要從源塊向目標塊提供訊息,源可以呼叫目標的 OfferData
方法。 OfferData
方法返回一個 DataflowMessageStatus
列舉,它具有多個可能的值:
-
已接受(Accepted):訊息已被接受並將由目標處理。
-
拒絕(Declined):該訊息被目標拒絕。 源塊仍然擁有該訊息,並且在當前訊息被另一個目標接受之前無法處理其下一條訊息。
-
永久拒絕(DecliningPermanently):訊息被拒絕,目標不再可用於處理。 所有後續訊息都將被當前目標拒絕。 源塊將取消與返回此狀態的目標的連結。
-
已推遲(Postponed):接受訊息已被推遲。 它可能會在稍後被目標接受。 在這種情況下,源可以等待或嘗試將訊息傳遞到備用目標塊。
-
無法使用(NotAvailable):當目標嘗試接受訊息時,該訊息不再可用。 當目標在訊息被推遲後嘗試接受訊息,但源塊已經將訊息傳遞給不同的目標塊時,可能會發生這種情況。
資料流塊透過提供 Complete
方法和 Completion
屬性來支援完成的概念。 呼叫 Complete
方法來請求塊的完成,而 Completion
屬性返回一個任務,稱為塊的完成任務。 這些完成成員是 IDataflowBlock
介面的一部分,該介面由 ISourceBlock
和 ITargetBlock
繼承。
完成任務可用於確定塊是否遇到錯誤或已被取消。 讓我們看看如何:
- 處理資料流塊遇到的錯誤的最簡單方法是呼叫塊的
Completion
屬性上的Wait
並在try/catch
塊中處理AggregateException
異常型別:
try
{
inputBlock.Completion.Wait();
}
catch (AggregateException ae)
{
ae.Handle(e =>
{
Console.WriteLine($"Error processing input - {e.GetType().Name}: {e.Message}");
});
}
- 如果您想在不使用阻塞
Wait
呼叫的情況下執行相同的操作,您可以等待完成任務並處理Exception
型別:
try
{
await inputBlock.Completion;
}
catch (Exception e)
{
Console.WriteLine($"Error processing input - {e.GetType().Name}: {e.Message}");
}
- 另一種選擇是在完成任務上使用
ContinueWith
方法。在延續塊內,您可以檢查任務的狀態以確定它是Faulted
還是Canceled
:
try
{
inputBlock.ContinueWith(task =>
{
Console.WriteLink($"Task completed with a status of {task.Status}");
});
await inputBlock.Completion;
}
catch (Exception e)
{
Console.WriteLine($"Error processing input - {e.GetType().Name}: {e.Message}");
}
當我們在下一節中使用生產者/消費者模式建立示例專案時,我們將看到更全面的資料流塊使用示例。 在我們研究資料流塊的型別之前,我們先討論一下 Microsoft 建立該庫的原因。
為什麼使用 TPL 資料流庫?
TPL 資料流庫由 Microsoft 建立,作為編排非同步資料處理工作流的一種手段。 資料從資料來源流入管道中的第一個資料流塊。 源可以是資料庫、本地或網路資料夾、相機或 .NET 可以訪問的任何其他型別的輸入裝置。 一個或多個塊可以是管道的一部分,每個塊負責一項操作。 下圖說明了資料流管道的兩個抽象:
您可以考慮的一個現實示例是使用網路攝像頭捕獲影像幀。 在兩步流程中,如示例 1 所示,將網路攝像頭視為資料輸入。 資料流塊 1 可以執行一些影像處理以最佳化影像外觀,而資料流塊 2 將呼叫 Azure 認知服務 API 來識別每個影像中的物件。 結果將包含每個輸入影像的新 .NET 類,其中包含影像二進位制資料和屬性,這些屬性包含每個影像中已識別的物件。
資料流塊的型別
資料流庫中有九個預定義塊。 這些可以分為三個不同的類別。 第一類是緩衝塊。
緩衝塊(Buffering blocks)
緩衝塊的目的是緩衝要消耗的輸入資料。 緩衝塊都是傳播器塊,這意味著它們既可以是資料流管道中的資料來源也可以是目標。 緩衝塊分為三種型別:BufferBlock<T>
、BroadcastBlock<T>
和 WriteOnceBlock<T>
。
緩衝塊(BufferBlock)
BufferBlock<T>
是一種非同步排隊機制,它實現物件的先進先出 (FIFO) 佇列。 BufferBlock
可以配置多個資料來源和多個目標。 但是,BufferBlock
中的每條訊息只能投遞到一個目標塊。成功投遞後,訊息將從佇列中刪除。
以下程式碼片段將客戶名稱推送到 BufferBlock
中,然後將前五個名稱讀取到控制檯:
BufferBlock<string> customerBlock = new();
foreach (var customer in customers)
{
await customerBlock.SendAsync(customer.Name);
}
for (int i = 0; i < 5; i++)
{
Console.WriteLine(await customerBlock.ReceiveAsync());
}
// The code could display the following output:
// Robert Jones
// Jita Smith
// Patty Xu
// Sam Alford
// Melissa Allen
廣播塊(BroadcastBlock)
BroadcastBlock<T>
的使用方式與 BufferBlock
類似,但它的目的是僅向消費者提供最近釋出的訊息。 它還可用於向許多消費者傳送相同的值。 釋出到 BroadcastBlock
的訊息在被消費者接收後不會被刪除。
每次呼叫 Receive
方法時,以下程式碼片段將讀取相同的警報訊息:
var alertBlock = new BroadcastBlock<string>(null);
alertBlock.Post("Network is unavailable!");
for (int i = 0; i < 5; i++)
{
Console.WriteLine(alertBlock.Receive());
}
一次寫入塊(WriteOnceBlock)
顧名思義,WriteOnceBlock<T>
只能寫入一次。 收到第一條訊息後,塊將忽略對 Post
或 SendAsync
的所有呼叫。 不會丟擲任何異常。 資料被簡單地丟棄。
以下示例與我們的 BufferBlock
程式碼片段類似。 但是,由於我們現在使用的是 WriteOnceBlock
,因此該塊僅接受第一個客戶的姓名:
WriteOnceBlock<string> customerBlock = new();
foreach (var customer in customers)
{
await customerBlock.SendAsync(customer.Name);
}
Console.WriteLine(await customerBlock.ReceiveAsync());
執行塊(Execution blocks)
執行塊是為收到的每條訊息執行委託方法的塊。 資料流庫中有三種型別的執行塊。 ActionBlock<TInput>
是目標塊,而 TransformBlock<TInput, TOuput>
和 TransformManyBlock<TInput, TOutput>
都是傳播器塊。
ActionBlock
ActionBlock
是一個接受 Action<T>
或 Func<TInput, Task>
作為其建構函式的塊。 當操作返回或 Func
的任務完成時,對輸入訊息的操作被視為完成。 您可以使用同步委託的操作或非同步操作的 Func。
在此程式碼片段中,我們將使用 Console.WriteLine
(在 Action
中提供)將客戶名稱輸出到控制檯:
var customerBlock = new ActionBlock<string>(name => Console.WriteLine(name));
foreach (var customer in customers)
{
await customerBlock.SendAsync(customer.Name);
}
customerBlock.Complete();
await customerBlock.Completion;
TransformBlock
TransformBlock<TInput, TOutput>
與 ActionBlock
類似。 但是,作為傳播器塊,它會為收到的每條訊息返回一個輸出值。 可以提供給 TransformBlock
建構函式的兩個可能的委託簽名是用於同步操作的 Func<TInput, TOutput>
和用於非同步操作的 Func<TInput, Task<TOutput>>
。
以下示例使用 TransformBlock
,在檢索前五個輸出值並顯示在控制檯上之前,該 TransformBlock
會將客戶名稱轉換為全部大寫:
var toUpperBlock = new TransformBlock<string, string>(name=> name.ToUpper());
foreach (var customer in customers)
{
toUpperBlock.Push(customer.Name);
}
for (int i = 0; i < 5; i++)
{
Console.WriteLine(toUpperBlock.Receive());
}
TransformManyBlock
TransformManyBlock
的簽名分別為用於同步和非同步操作的 Func<TInput, IEnumerable<TOutput>>
和 Func<TInput, Task<IEnumerable<TOutput>>>
。
在此程式碼片段中,我們將一個客戶名稱傳遞給 TransformManyBlock
,這將返回一個包含客戶名稱中各個字元的列舉:
var nameCharactersBlock = new TransformManyBlock<string,char>(name => name.ToCharArray());
nameCharactersBlock.Post(customerName);
for (int i = 0; i < (customerName.Length; i++)
{
Console.WriteLine(nameCharactersBlock.Receive());
}
分組塊(Grouping blocks)
分組塊(Grouping blocks)可以組合來自一個或多個源的物件。 分組塊分為三種型別: BatchBlock<T>
是傳播器塊,而 JoinBlock<T1, T2>
和 BatchedJoinBlock<T1, T2>
都是源塊。
BatchBlock
BatchBlock
接受批次資料並生成輸出資料陣列。 建立 BatchBlock
時,您可以指定輸入批次大小。 BatchBlock
在 dataflowBlockOptions
可選建構函式引數中有一個 Greedy
屬性,用於指定貪婪模式:
- 當
Greedy
為true
(這是其預設值)時,該塊會在接收到每個輸入值時繼續處理它,並在達到批次大小時輸出一個陣列。 - 當
Greedy
為false
時,可以在建立批次大小的陣列時暫停傳入訊息。
貪婪模式通常表現更好,但如果您要協調來自多個源的輸入,則可能需要使用非貪婪模式。
在此示例中,BatchBlock
將學生姓名分為最大大小為 12 的班級:
var studentBlock = new BatchBlock<string>(12);
// Assume studentList contains 20 students.
foreach (var student in studentList)
{
studentBlock.Post(student.Name);
}
// Signal that we are done adding items.
studentBlock.Complete();
// Print the size of each class.
Console.WriteLine($"The number of students in class 1 is {studentBlock.Receive().Count()}."); // 12 students
Console.WriteLine($"The number of students in class 2 is {studentBlock.Receive().Count()}."); // 8 students
JoinBlock
JoinBlock
有兩個簽名:JoinBlock<T1, T2> 和 JoinBlock<T1, T2, T3>
。JoinBlock<T1, T2>
具有 Target1
和 Target2
屬性來接受輸入並返回 Tuple<T1, T2>
,因為每對目標是 填充。 JoinBlock<T1, T2, T3>
具有 Target1
、Target2
和 Target3
屬性,並在每組目標完成時返回 Tuple<T1, T2, T3>
。
JoinBlock
還具有貪婪和非貪婪模式,其中貪婪模式是預設行為。當您切換到非貪婪模式時,所有輸入都會推遲到已經接收到輸入的目標,直到填充完整的輸出集並將其作為輸出傳送。
在此示例中,我們將建立一個 JoinBlock
將一個人的名字、姓氏和年齡組合到輸出元組中:
var joinBlock = new JoinBlock<string, string, int>();
joinBlock.Target1.Post("Sally");
joinBlock.Target1.Post("Raj");
joinBlock.Target2.Post("Jones");
joinBlock.Target2.Post("Gupta");
joinBlock.Target3.Post(7);
joinBlock.Target3.Post(23);
for (int i = 0; i < 2; i++)
{
var data = joinBlock.Receive();
if (data.Item3 < 18)
{
Console.WriteLine($"{data.Item1} {data.Item2} is a child.");
}
else
{
Console.WriteLine($"{data.Item1} {data.Item2} is an adult.");
}
}
BatchedJoinBlock
BatchedJoinBlock
類似於 JoinBlock
,只不過輸出中的元組包含建構函式中指定的批次大小的 IList
項:Tuple(IList(T1), IList(T2))
或 Tuple(IList(T1), IList(T2) ,IList(T3))
。 批處理概念與 BatchBlock
相同。
作為練習,嘗試在 JoinBlock
示例的基礎上新增更多人到列表中,將他們分為四批,並輸出每批中年齡最大的人的姓名。
現在我們已經探索了所有可用資料流塊的示例,讓我們進入一些現實世界的資料流示例。 在下一節中,我們將使用一些資料流塊來建立生產者/消費者實現。
實現生產者/消費者模式
TPL 資料流庫中的塊為實現生產者/消費者模式提供了一個絕佳的平臺。 如果您不熟悉這種設計模式,它涉及兩個操作和一個工作佇列。 生產者是第一個操作。 它負責用資料或工作單元填充佇列。 消費者負責從佇列中取出專案並以某種方式對其進行操作。 系統中可以有一個或多個生產者和一個或多個消費者。 您可以更改生產者或消費者的數量,具體取決於流程的哪個部分是瓶頸。
在我們的 .NET 生產者/消費者示例中,我們將構建一個簡單的 WPF 應用程式,該應用程式從多個 RSS 源獲取部落格文章並將其顯示在單個 ListView
控制元件中。 應用程式中生產者的每一行將從 RSS 提要中獲取帖子,並將 SyndicateItem
新增到每個部落格帖子的佇列中。 我們將從三個部落格獲取帖子,併為每個部落格建立一個製作人。
消費者將從佇列中獲取一個 SyndicateItem
,並使用 ActionBlock
委託為每個 SyndicateItem
建立一個 BlogPost
物件。 我們將建立三個消費者來跟上三個生產者排隊的專案。 當該過程完成時,BlogPost
物件的列表將被設定為 ListView
的 ItemSource
。 讓我們開始吧:
-
首先使用 .NET 6 建立一個新的 WPF 專案。將專案命名為
ProducerConsumerRssFeeds
。 -
開啟該解決方案的 NuGet 包管理器,在“安裝”選項卡上搜尋“聚合”,然後將
System.ServiceModel.Synmination
包新增到專案中。 該包將使從任何 RSS 提要獲取資料變得簡單。 -
向名為
BlogPost
的專案新增一個新類。 這將是要在 ListView 中顯示的每篇部落格文章的模型物件。 將以下屬性新增到新類中:
public class BlogPost
{
public string PostDate { get; set; } = "";
public string? Categories { get; set; }
public string? PostContent { get; set; }
}
- 現在,是時候建立一個服務類來獲取給定 RSS 提要 URL 的部落格文章了。 將名為
RssFeedService
的新類新增到專案中,並向該類新增名為GetFeedItems
的方法:
using System.Collections.Generic;
using System.ServiceModel.Syndication;
using System.Xml;
//...
public static IEnumerable<SyndicationItem>
GetFeedItems(string feedUrl)
{
using var xmlReader = XmlReader.Create(feedUrl);
SyndicationFeed rssFeed = SyndicationFeed.Load(xmlReader);
return rssFeed.Items;
}
靜態 SyndicateFeed.Load
方法使用 XmlReader
從提供的 feedUrl
中獲取 XML,並將其轉換為 IEnumerable<SyndicateItem>
以從該方法返回。
- 接下來,建立一個名為
FeedAggregator
的新類。 此類將包含生產者/消費者邏輯,該邏輯為每個部落格呼叫GetFeedItems
並轉換每個部落格文章的提要資料,以便可以在 UI 中顯示。 我們聚合的三個部落格如下:
-
.NET 部落格
-
Windows 部落格
-
Microsoft 365 部落格
使用 FeedAggregator
的第一步是建立一個名為 ProduceFeedItems
的生產者方法和一個名為 QuseueAllFeeds
的父方法,該方法將啟動該生產者方法的三個例項:
private async Task QueueAllFeeds(BufferBlock
<SyndicationItem> itemQueue)
{
Task feedTask1 = ProduceFeedItems(itemQueue,"https://devblogs.microsoft.com/dotnet/feed/");
Task feedTask2 = ProduceFeedItems(itemQueue,"https://blogs.windows.com/feed");
Task feedTask3 = ProduceFeedItems(itemQueue,"https://www.microsoft.com/microsoft-365/blog/feed/");
await Task.WhenAll(feedTask1, feedTask2,feedTask3);
itemQueue.Complete();
}
private async Task ProduceFeedItems(BufferBlock<SyndicationItem> itemQueue, string feedUrl)
{
IEnumerable<SyndicationItem> items =
RssFeedService.GetFeedItems(feedUrl);
foreach (SyndicationItem item in items)
{
await itemQueue.SendAsync(item);
}
}
我們使用 BufferBlock<SyndicateItem>
作為佇列。 每個生產者都會呼叫 GetFeedItems
並將返回到 BufferBlock
的每個 SyndicateItem
新增到一起。 QueueAllFeeds
方法使用 Task.WhenAll
等待所有生產者完成向佇列新增專案。 然後,它向 BufferBlock
發出訊號,表明所有生產者都已透過呼叫 itemQueue.Complete()
完成。
- 接下來,我們將建立我們的消費者方法。 該方法名為
ConsumeFeedItem
,將負責獲取BufferBlock
提供的SyndicateItem
並將其轉換為BlogPost
物件。 每個BlogPost
都將新增到ConcurrentBag<BlogPost>
中。 我們在這裡使用執行緒安全集合,因為會有多個使用者將輸出新增到列表中:
private void ConsumeFeedItem(SyndicationItem nextItem,
ConcurrentBag<BlogPost> posts)
{
if (nextItem != null && nextItem.Summary != null)
{
BlogPost newPost = new();
newPost.PostContent = nextItem.Summary.Text
.ToString();
newPost.PostDate = nextItem.PublishDate
.ToLocalTime().ToString("g");
if (nextItem.Categories != null)
{
newPost.Categories = string.Join(",",
nextItem.Categories.Select(c =>
c.Name));
}
posts.Add(newPost);
}
}
- 現在,是時候將生產者/消費者邏輯連線在一起了。 建立一個名為
GetAllMicrosoftBlogPosts
的方法:
public async Task<IEnumerable<BlogPost>>
GetAllMicrosoftBlogPosts()
{
var posts = new ConcurrentBag<BlogPost>();
// Create queue of source posts
BufferBlock<SyndicationItem> itemQueue = new(new
DataflowBlockOptions { BoundedCapacity =
10 });
// Create and link consumers
var consumerOptions = new ExecutionDataflowBlockOptions { BoundedCapacity = 1 };
var consumerA = new ActionBlock<SyndicationItem>
((i) => ConsumeFeedItem(i, posts), consumerOptions);
var consumerB = new ActionBlock<SyndicationItem>
((i) => ConsumeFeedItem(i, posts), consumerOptions);
var consumerC = new ActionBlock<SyndicationItem>
((i) => ConsumeFeedItem(i, posts), consumerOptions);
var linkOptions = new DataflowLinkOptions { PropagateCompletion = true, };
itemQueue.LinkTo(consumerA, linkOptions);
itemQueue.LinkTo(consumerB, linkOptions);
itemQueue.LinkTo(consumerC, linkOptions);
// Start producers
Task producers = QueueAllFeeds(itemQueue);
// Wait for producers and consumers to complete
await Task.WhenAll(producers, consumerA.Completion,
consumerB.Completion, consumerC.Completion);
return posts;
}
I. 該方法首先建立一個 ConcurrentBag<BlogPost>
來聚合 UI 的最終帖子列表。 然後,它建立 BoundedCapacity
為 10
的 itemQueue 物件。此有限容量意味著任何時候排隊的專案數不得超過 10
個。 一旦佇列達到 10
個,所有生產者都必須等待消費者將某些專案出隊。 這可能會降低程序的效能,但可以防止生產程式碼中潛在的記憶體不足問題。 我們的示例在處理來自三個部落格的帖子時不會有記憶體不足的危險,但您可以瞭解如何在應用程式中需要時使用 BoundedCapacity
。 您可以建立沒有 BoundedCapacity
的佇列,如下所示:
BufferBlock<SyndicationItem> itemQueue = new();
二. 該方法的下一部分建立三個使用 ActionBlock<SyndicateItem>
的使用者,並以 ConsumeFeedItem
作為提供的委託。 每個消費者都透過 LinkTo
方法連結到佇列。將消費者的 BoundedCapacity
設定為 1
告訴生產者如果當前消費者已經忙於處理一項,則繼續處理下一個消費者。
三. 一旦建立了連結,我們就可以透過呼叫 QueueAllFeeds
來啟動生產者。 然後,我們必須等待每個消費者 ActionBlock
的生產者和 Completion
物件。 透過連結生產者和消費者的完成,我們不需要顯式等待消費者的 Completion
物件:
var linkOptions = new DataflowLinkOptions {PropagateCompletion = true, };
- 下一步是建立一些 UI 控制元件來向使用者顯示資訊。 開啟
MainWindow.xaml
檔案並使用以下標記替換現有的 Grid:
<Grid>
<ListView x:Name="mainListView">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="150"/>
<ColumnDefinition
Width="300"/>
<ColumnDefinition
Width="500"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding PostDate}"
Margin="3"/>
<TextBox IsReadOnly="True"
Grid.Column="1"
Text="{Binding Categories}"
Margin="3"
TextWrapping="Wrap"/>
<TextBox IsReadOnly="True"
Grid.Column="2"
Text="{Binding PostContent}"
Margin="3"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
- 我們必須做的最後一件事是呼叫
GetAllMicrosoftBlogPosts
方法並填充 UI。 開啟MainWindow.xaml.cs
並新增以下程式碼:
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
}
private async void MainWindow_Loaded(object sender,
RoutedEventArgs e)
{
FeedAggregator aggregator = new();
var items = await aggregator
.GetAllMicrosoftBlogPosts();
mainListView.ItemsSource = items;
}
載入 MainWindow
後,從 GetAllMicrosoftBlogPosts
返回的專案將設定為 mainListView.ItemsSource
。這將允許資料繫結到我們在 XAML 中定義的 DataTemplate
中的元素。
- 現在,執行該專案並檢視效果:
圖 7.2 – 首次執行 ProducerConsumerRssFeeds WPF 應用程式
如您所見,該列表顯示每個 Microsoft 部落格的 10 篇部落格文章摘要。 這是 Microsoft 部落格可以返回的預設最大專案數。
您可以嘗試透過增加或減少專案中生產者和消費者的數量來進行試驗。 新增更多消費者是否會加快該過程? 嘗試新增一些你最喜歡的
部落格的提要到生產者列表,看看會發生什麼。
建立具有多個塊的資料管道
使用資料流塊的最大優勢之一是能夠連結它們並建立完整的工作流或資料管道。 在上一節中,我們瞭解了生產者塊和消費者塊之間的這種連結是如何工作的。 在本節中,我們將建立一個控制檯應用程式,其中包含五個資料流塊的管道,所有資料流塊都連結在一起以完成一系列任務。 我們將利用 TransformBlock
、TransformManyBlock
和 ActionBlock
獲取 RSS 提要並輸出提要中所有部落格文章中唯一的類別列表。 按著這些次序:
-
首先在 Visual Studio 中建立一個名為
OutputBlogCategories
的新 .NET 6 控制檯應用程式。 -
新增我們在上一個示例中使用的
System.ComponentModel.Synmination
NuGet 包。 -
新增與上一示例相同的
RssFeedService
類。 您可以在解決方案資源管理器中右鍵單擊該專案,然後選擇 新增| 現有專案,或者您可以建立一個名為RssFeedService
的新類,並複製/貼上我們在上一個示例中使用的相同程式碼。 -
將名為
FeedCategoryTransformer
的新類新增到專案中,並建立名為GetCategoriesForFeed
的方法:
public static async Task GetCategoriesForFeed(string
url)
{
}
- 在接下來的幾個步驟中,我們將建立
GetCategoriesForFeed
方法的實現。 首先,建立一個名為downloadFeed
的TransformBlock
,它接受字串形式的 url 並從GetFeedItems
方法返回IEnumerable<SyndicateItem>
:
// Downloads the requested blog posts.
var downloadFeed = new TransformBlock<string,
IEnumerable<SyndicationItem>>(url =>
{
Console.WriteLine("Fetching feed from '{0}'...",
url);
return RssFeedService.GetFeedItems(url);
});
- 接下來,建立一個接受
IEnumerable<SyndicateItem>
並返回List<SyndicateCategory>
的TransformBlock
。 此塊將從每個部落格文章中獲取完整的類別列表,並將它們作為單個列表返回:
// Aggregates the categories from all the posts.
var createCategoryList = new TransformBlock
<IEnumerable<SyndicationItem>, List
<SyndicationCategory>>(items =>
{
Console.WriteLine("Getting category list...");
var result = new List<SyndicationCategory>();
foreach (var item in items)
{
result.AddRange(item.Categories);
}
return result;
});
- 現在,建立另一個
TransformBlock
。 此塊將接受前一個塊中的List<SyndicateCategory>
,刪除所有重複項,並返回過濾後的List<SyndicateCategory>
:
// Removes duplicates.
var deDupList = new TransformBlock<List
<SyndicationCategory>, List<SyndicationCategory>>
(categories =>
{
Console.WriteLine("De-duplicating category list...");
var categoryComparer = new CategoryComparer();
return categories.Distinct(categoryComparer)
.ToList();
});
要在複雜物件(例如 SyndicateCategory
)上使用 LINQ Distinct
擴充套件方法,需要一個實現 IEqualityComparer<T>
的自定義比較器。
- 接下來,建立一個名為
createCategoryString
的TransformManyBlock
。 此塊將接受去重複的List<SyndicateCategory>
併為類別的每個Name
屬性返回一個字串。 因此,該塊會針對整個列表呼叫一次,但反過來,它也會為列表中的每個專案呼叫流程中的下一個塊:
// Gets the category names from the list of category
objects.
var createCategoryString = new TransformManyBlock
<List<SyndicationCategory>, string>(categories =>
{
Console.WriteLine("Extracting category names...");
return categories.Select(c => c.Name);
});
- 最後一個塊是名為
printCategoryInCaps
的ActionBlock
。 此塊將使用ToUpper
將每個類別名稱全部大寫輸出到控制檯:
// Prints the upper-cased unique categories to the
console.
var printCategoryInCaps = new ActionBlock<string>
(categoryName =>
{
Console.WriteLine($"Found CATEGORY {categoryName.ToUpper()}");
});
- 現在資料流塊已經配置完畢,是時候連結它們了。 建立一個
DataflowLinkOptions
來傳播每個塊的完成情況。 然後,使用LinkTo
方法將鏈中的每個塊連結到下一個塊:
var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };
downloadFeed.LinkTo(createCategoryList, linkOptions);
createCategoryList.LinkTo(deDupList, linkOptions);
deDupList.LinkTo(createCategoryString, linkOptions);
createCategoryString.LinkTo(printCategoryInCaps, linkOptions);
- 建立
GetCategoriesForFeed
方法的最後幾個步驟涉及將url
傳送到第一個塊,將其標記為Complete
,然後等待鏈中的最後一個塊:
await downloadFeed.SendAsync(url);
downloadFeed.Complete();
await printCategoryInCaps.Completion;
- 現在,開啟
Program.cs
並更新程式碼,以便它呼叫GetCategoriesForFeed
,提供 Windows 部落格 RSS 源的 URL:
using OutputBlogCategories;
Console.WriteLine("Hello, World!");
await FeedCategoryTransformer.GetCategoriesForFeed("https://blogs.windows.com/feed");
Console.ReadLine();
- 執行程式並檢查輸出中的類別列表:
圖 7.3 – 顯示 Windows 部落格源中的已消除重複的類別列表
現在您已經瞭解瞭如何使用一系列資料流塊建立資料管道,我們將看一個使用 JoinBlock
組合來自多個源的資料的示例。
操作來自多個資料來源的資料
JoinBlock
可以配置為從兩個或三個資料來源接收不同的資料型別。 當每組資料型別完成時,該塊就完成了一個包含要操作的所有三種物件型別的元組。 在此示例中,我們將建立一個接受字串和整數對的 JoinBlock
,並將 Tuple(string, int) 傳遞給 ActionBlock
,後者將它們的值輸出到控制檯。 按著這些次序:
-
首先在 Visual Studio 中建立一個新的控制檯應用程式
-
在專案中新增一個名為
DataJoiner
的新類,並在名為JoinData
的類中新增一個靜態方法:public static void JoinData() { }
-
新增以下程式碼以建立兩個
BufferBlock
物件:JoinBlock<string,int>
和ActionBlock<Tuple<string, int>>
:var stringQueue = new BufferBlock<string>(); var integerQueue = new BufferBlock<int>(); var joinStringsAndIntegers = new JoinBlock<string,int>( new GroupingDataflowBlockOptions { Greedy = false }); var stringIntegerAction = new ActionBlock <Tuple<string, int>>(data => { Console.WriteLine($"String received:{data.Item1}"); Console.WriteLine($"Integer received:{data.Item2}"); });
將塊設定為非貪婪模式意味著它將在執行塊之前等待每種型別的專案。
-
現在,建立塊之間的連結:
stringQueue.LinkTo(joinStringsAndIntegers.Target1); integerQueue.LinkTo(joinStringsAndIntegers.Target2); joinStringsAndIntegers.LinkTo(stringIntegerAction);
-
接下來,將一些資料推送到兩個
BufferBlock
物件,等待一秒鐘,然後將它們都標記為完成:stringQueue.Post("one"); stringQueue.Post("two"); stringQueue.Post("three"); integerQueue.Post(1); integerQueue.Post(2); integerQueue.Post(3); stringQueue.Complete(); integerQueue.Complete(); Thread.Sleep(1000); Console.WriteLine("Complete");
-
在
Program.cs
中新增以下程式碼來執行示例程式碼:using JoinBlockExample; DataJoiner.JoinData(); Console.ReadLine();
-
最後,執行應用程式並檢查輸出。 您將看到
ActionBlock
為提供的每組值輸出一個字串和整數對:圖 7.4 – 執行 JoinBlockExample 控制檯應用程式
這就是使用 JoinBlock
資料流塊的全部內容。 嘗試自己進行一些更改,例如更改 Greedy
選項或將資料新增到每個 BufferBlock
的順序。這對輸出有何影響?