[英]How do I split and merge this dataflow pipeline?
我正在嘗試使用具有以下形式的 tpl 創建數據流:
-> LoadDataBlock1 -> ProcessDataBlock1 ->
GetInputPathsBlock -> LoadDataBlock2 -> ProcessDataBlock2 -> MergeDataBlock -> SaveDataBlock
-> LoadDataBlock3 -> ProcessDataBlock3 ->
...
-> LoadDataBlockN -> ProcessDataBlockN ->
這個想法是, GetInputPathsBlock
是一個塊,它找到要加載的輸入數據的路徑,然后將路徑發送到每個LoadDataBlock
。 LoadDataBlocks 都是相同的(除了它們每個都從 GetInputPaths 接收到一個唯一的 inputPath 字符串)。 然后將加載的數據發送到ProcessDataBlock
,它會進行一些簡單的處理。 然后來自每個ProcessDataBlock
的數據被發送到MergeDataBlock
,MergeDataBlock 合並它並將其發送到SaveDataBlock
,然后將其保存到文件中。
將其視為需要每個月運行的數據流。 首先為每天的數據找到路徑。 每天的數據都被加載和處理,然后在整個月中合並在一起並保存。 每個月都可以並行運行,一個月中每一天的數據可以並行加載並並行處理(在單個日期數據加載完成后),當月的所有內容都加載並處理完畢后,可以合並並保存.
我試過的
據我所知TransformManyBlock<TInput,string>
可用於進行拆分( GetInputPathsBlock
),並且可以鏈接到普通的TransformBlock<string,InputData>
( LoadDataBlock
),然后從那里鏈接到另一個TransformBlock<InputData,ProcessedData>
( ProcessDataBlock
),但我不知道如何將其合並回單個塊。
我看過的
我找到了這個答案,它使用TransformManyBlock
到 go 從IEnumerable<item>
到item
,但我不完全理解它,我無法將TransformBlock<InputData,ProcessedData>
( ProcessDataBlock
)鏈接到TransformBlock<IEnumerable<ProcessedData>>,ProcessedData>
,所以不知道怎么用。
我也看到過這樣的答案,它建議使用JoinBlock
,但是輸入文件的數量 N 不同,並且文件都以相同的方式加載。
還有這個答案,它似乎做了我想要的,但我不完全理解它,我不知道如何將字典的設置轉移到我的案例中。
如何拆分和合並我的數據流?
TransformManyBlock
兩次嗎? 我會使用嵌套塊來避免拆分我的每月數據,然后再次合並它們。 以下是處理 2020 年所有日子的兩個嵌套TransformBlock
的示例:
var monthlyBlock = new TransformBlock<int, List<string>>(async (month) =>
{
var dailyBlock = new TransformBlock<int, string>(async (day) =>
{
await Task.Delay(100); // Simulate async work
return day.ToString();
}, new ExecutionDataflowBlockOptions() { MaxDegreeOfParallelism = 4 });
foreach (var day in Enumerable.Range(1, DateTime.DaysInMonth(2020, month)))
await dailyBlock.SendAsync(day);
dailyBlock.Complete();
var dailyResults = await dailyBlock.ToListAsync();
return dailyResults;
}, new ExecutionDataflowBlockOptions() { MaxDegreeOfParallelism = 1 });
foreach (var month in Enumerable.Range(1, 12))
await monthlyBlock.SendAsync(month);
monthlyBlock.Complete();
為了收集內部塊的每日結果,我使用了擴展方法ToListAsync
,如下所示:
public static async Task<List<T>> ToListAsync<T>(this IReceivableSourceBlock<T> block,
CancellationToken cancellationToken = default)
{
var list = new List<T>();
while (await block.OutputAvailableAsync(cancellationToken).ConfigureAwait(false))
{
while (block.TryReceive(out var item))
{
list.Add(item);
}
}
await block.Completion.ConfigureAwait(false); // Propagate possible exception
return list;
}
您的問題的答案是:不,您不需要其他塊類型,是的,您可以使用 TransformManyBlock 兩次,是的,它確實有意義。 我寫了一些代碼來證明它,它在底部,還有一些關於它如何工作的注釋,在這之后。
正如您所描述的,該代碼使用拆分然后合並管道。 至於您正在努力解決的問題:可以通過將已處理的項目添加到列表中來完成將各個文件的數據合並在一起。 然后,如果它具有預期的最終項目數,我們只將列表傳遞到下一個塊。 這可以通過返回零或一項的相當簡單的 TransformMany 塊來完成。 此塊無法並行化,因為該列表不是線程安全的。
一旦你有了這樣的管道,你就可以通過使用傳遞給塊的選項來測試並行化和排序。 下面的代碼將每個塊的並行化設置為無界,並讓 DataFlow 代碼對其進行排序。 在我的機器上,它最大化了所有內核/邏輯處理器,並且受 CPU 限制,這正是我們想要的。 排序已啟用,但關閉它並沒有太大區別:同樣,我們受 CPU 限制。
最后,我不得不說這是一項非常酷的技術,但您實際上可以更簡單地使用 PLINQ 解決這個問題,只需幾行代碼即可獲得同樣快的結果。 最大的缺點是,如果這樣做,您將無法輕松地將快速到達的消息逐步添加到管道中:PLINQ 更適合一個大批量進程。 但是,對於您的用例,PLINQ 可能是更好的解決方案。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks.Dataflow;
namespace ParallelDataFlow
{
class Program
{
static void Main(string[] args)
{
new Program().Run();
Console.ReadLine();
}
private void Run()
{
Stopwatch s = new Stopwatch();
s.Start();
// Can experiment with parallelization of blocks by changing MaxDegreeOfParallelism
var options = new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded };
var getInputPathsBlock = new TransformManyBlock<(int, int), WorkItem>(date => GetWorkItemWithInputPath(date), options);
var loadDataBlock = new TransformBlock<WorkItem, WorkItem>(workItem => LoadDataIntoWorkItem(workItem), options);
var processDataBlock = new TransformBlock<WorkItem, WorkItem>(workItem => ProcessDataForWorkItem(workItem), options);
var waitForProcessedDataBlock = new TransformManyBlock<WorkItem, List<WorkItem>>(workItem => WaitForWorkItems(workItem)); // Can't parallelize this block
var mergeDataBlock = new TransformBlock<List<WorkItem>, List<WorkItem>>(list => MergeWorkItemData(list), options);
var saveDataBlock = new ActionBlock<List<WorkItem>>(list => SaveWorkItemData(list), options);
var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };
getInputPathsBlock.LinkTo(loadDataBlock, linkOptions);
loadDataBlock.LinkTo(processDataBlock, linkOptions);
processDataBlock.LinkTo(waitForProcessedDataBlock, linkOptions);
waitForProcessedDataBlock.LinkTo(mergeDataBlock, linkOptions);
mergeDataBlock.LinkTo(saveDataBlock, linkOptions);
// We post individual tuples of (year, month) to our pipeline, as many as we want
getInputPathsBlock.Post((1903, 2)); // Post one month and date
var dates = from y in Enumerable.Range(2015, 5) from m in Enumerable.Range(1, 12) select (y, m);
foreach (var date in dates) getInputPathsBlock.Post(date); // Post a big sequence
getInputPathsBlock.Complete();
saveDataBlock.Completion.Wait();
s.Stop();
Console.WriteLine($"Completed in {s.ElapsedMilliseconds}ms on {ThreadAndTime()}");
}
private IEnumerable<WorkItem> GetWorkItemWithInputPath((int year, int month) date)
{
List<WorkItem> processedWorkItems = new List<WorkItem>(); // Will store merged results
return GetInputPaths(date.year, date.month).Select(
path => new WorkItem
{
Year = date.year,
Month = date.month,
FilePath = path,
ProcessedWorkItems = processedWorkItems
});
}
// Get filepaths of form e.g. Files/20191101.txt These aren't real files, they just show how it could work.
private IEnumerable<string> GetInputPaths(int year, int month) =>
Enumerable.Range(0, GetNumberOfFiles(year, month)).Select(i => $@"Files/{year}{Pad(month)}{Pad(i + 1)}.txt");
private int GetNumberOfFiles(int year, int month) => DateTime.DaysInMonth(year, month);
private WorkItem LoadDataIntoWorkItem(WorkItem workItem) {
workItem.RawData = LoadData(workItem.FilePath);
return workItem;
}
// Simulate loading by just concatenating to path: in real code this could open a real file and return the contents
private string LoadData(string path) => "This is content from file " + path;
private WorkItem ProcessDataForWorkItem(WorkItem workItem)
{
workItem.ProcessedData = ProcessData(workItem.RawData);
return workItem;
}
private string ProcessData(string contents)
{
Thread.SpinWait(11000000); // Use 11,000,000 for ~50ms on Windows .NET Framework. 1,100,000 on Windows .NET Core.
return $"Results of processing file with contents '{contents}' on {ThreadAndTime()}";
}
// Adds a processed WorkItem to its ProcessedWorkItems list. Then checks if the list has as many processed WorkItems as we
// expect to see overall. If so the list is returned to the next block, if not we return an empty array, which passes nothing on.
// This isn't threadsafe for the list, so has to be called with MaxDegreeOfParallelization = 1
private IEnumerable<List<WorkItem>> WaitForWorkItems(WorkItem workItem)
{
List<WorkItem> itemList = workItem.ProcessedWorkItems;
itemList.Add(workItem);
return itemList.Count == GetNumberOfFiles(workItem.Year, workItem.Month) ? new[] { itemList } : new List<WorkItem>[0];
}
private List<WorkItem> MergeWorkItemData(List<WorkItem> processedWorkItems)
{
string finalContents = "";
foreach (WorkItem workItem in processedWorkItems)
{
finalContents = MergeData(finalContents, workItem.ProcessedData);
}
// Should really create a new data structure and return that, but let's cheat a bit
processedWorkItems[0].MergedData = finalContents;
return processedWorkItems;
}
// Just concatenate the output strings, separated by newlines, to merge our data
private string MergeData(string output1, string output2) => output1 != "" ? output1 + "\n" + output2 : output2;
private void SaveWorkItemData(List<WorkItem> workItems)
{
WorkItem result = workItems[0];
SaveData(result.MergedData, result.Year, result.Month);
// Code to show it's worked...
Console.WriteLine($"Saved data block for {DateToString((result.Year, result.Month))} on {ThreadAndTime()}." +
$" File contents:\n{result.MergedData}\n");
}
private void SaveData(string finalContents, int year, int month)
{
// Actually save, although don't really need to in this test code
new DirectoryInfo("Results").Create();
File.WriteAllText(Path.Combine("Results", $"results{year}{Pad(month)}.txt"), finalContents);
}
// Helper methods
private string DateToString((int year, int month) date) => date.year + Pad(date.month);
private string Pad(int number) => number < 10 ? "0" + number : number.ToString();
private string ThreadAndTime() => $"thread {Pad(Thread.CurrentThread.ManagedThreadId)} at {DateTime.Now.ToString("hh:mm:ss.fff")}";
}
public class WorkItem
{
public int Year { get; set; }
public int Month { get; set; }
public string FilePath { get; set; }
public string RawData { get; set; }
public string ProcessedData { get; set; }
public List<WorkItem> ProcessedWorkItems { get; set; }
public string MergedData { get; set; }
}
}
此代碼將 WorkItem object 從每個塊傳遞到下一個塊,並在每個階段對其進行豐富。 然后它會創建一個包含一個月內所有 WorkItems 的最終列表,然后對其運行聚合過程並保存結果。
此代碼基於使用您使用的名稱的每個階段的虛擬方法。 這些並沒有做太多,但希望能證明解決方案。 例如,LoadData 被傳遞一個文件路徑,只是向其中添加一些文本並傳遞字符串,但顯然它可以加載一個真實文件並傳遞內容字符串,如果磁盤上確實有一個文件。
與在 ProcessData 中模擬工作類似,我們執行 Thread.SpinWait,然后再次向字符串添加一些文本。 這就是延遲的來源,因此如果您希望它運行得更快或更慢,請更改數字。 該代碼是在 .NET 框架上編寫的,但它在 Core 3.0 以及 Ubuntu 和 OSX 上運行。 唯一的區別是 SpinWait 周期可以明顯更長或更短,因此您可能希望使用延遲。
請注意,我們可以在 waitForProcessedDataBlock 中合並,並擁有您所要求的管道。 只是會更混亂一些
該代碼最終會在磁盤上創建文件,但也會將結果轉儲到屏幕上,因此它實際上並不需要。
如果您將並行化設置為 1,您會發現它會減慢您預期的速度。 我的 Windows 機器是四核的,它比慢四倍略差。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.