簡體   English   中英

對於 TPL 數據流:如何在阻塞直到處理完所有輸入之前獲得 TransformBlock 產生的所有輸出?

[英]For a TPL Dataflow: How do I get my hands on all the output produced by a TransformBlock while blocking until all inputs have been processed?

我正在向單個數據庫同步提交一系列select語句(查詢 - 數千個),並為每個查詢返回一個DataTable (注意:該程序知道它僅在運行時掃描的數據庫模式,因此使用DataTables )。 該程序在客戶端機器上運行並連接到遠程機器上的數據庫。 運行這么多查詢需要很長時間。 因此,假設異步或並行執行它們會加快速度,我正在探索TPL Dataflow (TDF) 我想使用TDF庫,因為它似乎可以處理與編寫多線程代碼相關的所有問題,否則這些問題需要手動完成。

顯示的代碼基於http://blog.i3arnon.com/2016/05/23/tpl-dataflow/ 它是最小的,只是為了幫助我理解TDF的基本操作。 請知道我已經閱讀了許多博客並編寫了許多迭代來試圖破解這個堅果。

盡管如此,在當前的迭代中,我有一個問題和一個問題:

問題

代碼位於button click方法中(使用 UI,用戶選擇一台機器、一個 sql 實例和一個數據庫,然后開始掃描)。 帶有await運算符的兩行在構建時返回錯誤: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task' The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task' 我無法更改按鈕單擊方法的返回類型。 我是否需要以某種方式將button click方法與async-await代碼隔離開來?

問題

盡管我找到了描述TDF基礎知識的漂亮文章,但我找不到一個示例來說明如何掌握每次調用TransformBlock產生的輸出(即DataTable )。 雖然我想提交查詢async ,但我確實需要阻止,直到提交給TransformBlock的所有查詢都完成。 如何獲得由TransformBlock生成的一系列DataTable並阻止直到所有查詢完成?

注意:我承認我現在只有一個塊。 至少,我將添加一個取消塊,因此需要/想要使用 TPL。

private async Task ToolStripButtonStart_Click(object sender, EventArgs e)
{

    UserInput userInput = new UserInput
    {
        MachineName = "gat-admin",
        InstanceName = "",
        DbName = "AdventureWorks2014",
    };

    DataAccessLayer dataAccessLayer = new DataAccessLayer(userInput.MachineName, userInput.InstanceName);

    //CreateTableQueryList gets a list of all tables from the DB and returns a list of 
    // select statements, one per table, e.g., SELECT * from [schemaname].[tablename]
    IList<String> tableQueryList = CreateTableQueryList(userInput);

    // Define a block that accepts a select statement and returns a DataTable of results
    // where each returned record is: schemaname + tablename + columnname + column datatype + field data
    // e.g., if the select query returns one record with 5 columns, then a datatable with 5 
    // records (one per field) will come back 

    var transformBlock_SubmitTableQuery = new TransformBlock<String, Task<DataTable>>(
        async tableQuery => await dataAccessLayer._SubmitSelectStatement(tableQuery),
        new ExecutionDataflowBlockOptions
        {
            MaxDegreeOfParallelism = 2,
        });

    // Add items to the block and start processing
    foreach (String tableQuery in tableQueryList)
    {
        await transformBlock_SubmitTableQuery.SendAsync(tableQuery);
    }

    // Enable the Cancel button and disable the Start button.
    toolStripButtonStart.Enabled = false;
    toolStripButtonStop.Enabled = true;

    //shut down the block (no more inputs or outputs)
    transformBlock_SubmitTableQuery.Complete();

    //await the completion of the task that procduces the output DataTable
    await transformBlock_SubmitTableQuery.Completion;
}

public async Task<DataTable> _SubmitSelectStatement(string queryString )
{
    try
    {

        .
        .
        await Task.Run(() => sqlDataAdapter.Fill(dt));

        // process dt into the output DataTable I need

        return outputDt;
    }
    catch
    {
        throw;
    }

}

檢索TransformBlock輸出的最簡潔方法是使用方法OutputAvailableAsyncTryReceive執行嵌套循環。 它有點冗長,因此您可以考慮將此功能封裝在擴展方法ToListAsync

public static async Task<List<T>> ToListAsync<T>(this IReceivableSourceBlock<T> source,
    CancellationToken cancellationToken = default)
{
    var list = new List<T>();
    while (await source.OutputAvailableAsync(cancellationToken).ConfigureAwait(false))
    {
        while (source.TryReceive(out var item))
        {
            list.Add(item);
        }
    }
    await source.Completion.ConfigureAwait(false); // Propagate possible exception
    return list;
}

然后你可以像這樣使用ToListAsync方法:

private async Task ToolStripButtonStart_Click(object sender, EventArgs e)
{
    var transformBlock = new TransformBlock<string, DataTable>(async query => //...
    //...
    transformBlock.Complete();

    foreach (DataTable dataTable in await transformBlock.ToListAsync())
    {
        // Do something with each dataTable
    }
}

注意:這個ToListAsync實現是破壞性的,這意味着如果發生錯誤,消耗的消息將被丟棄。 要使其成為非破壞性的,只需刪除await source.Completion行。 在這種情況下,您必須記住在處理完包含已消費消息的列表后await塊的Completion ,否則您將不會知道TransformBlock是否未能處理其所有輸入。

確實存在檢索數據流塊輸出的替代方法,例如 dcastro 的這個使用BufferBlock作為緩沖區並且性能稍高一些,但我個人認為上述方法更安全、更直接。

除了在檢索輸出之前等待塊完成,您還可以以流方式檢索它,作為IAsyncEnumerable<T>序列:

public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(
    this IReceivableSourceBlock<T> source,
    [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    while (await source.OutputAvailableAsync(cancellationToken).ConfigureAwait(false))
    {
        while (source.TryReceive(out var item))
        {
            yield return item;
            cancellationToken.ThrowIfCancellationRequested();
        }
    }
    await source.Completion.ConfigureAwait(false); // Propagate possible exception
}

這樣一來,您就可以在每個DataTable煮熟后立即獲得它,而無需等待所有查詢的處理。 要使用IAsyncEnumerable<T> ,您只需將await移到foreach之前:

await foreach (DataTable dataTable in transformBlock.ToAsyncEnumerable())
{
    // Do something with each dataTable
}

高級:下面是ToListAsync方法的更復雜版本,它傳播底層塊的所有錯誤。 原始的簡單ToListAsync方法僅傳播第一個錯誤。

/// <summary>
/// Asynchronously waits for the successful completion of the specified source, and
/// returns all the received messages. In case the source completes with error,
/// the error is propagated and the received messages are discarded.
/// </summary>
public static Task<List<T>> ToListAsync<T>(this IReceivableSourceBlock<T> source,
    CancellationToken cancellationToken = default)
{
    ArgumentNullException.ThrowIfNull(source);

    async Task<List<T>> Implementation()
    {
        var list = new List<T>();
        while (await source.OutputAvailableAsync(cancellationToken)
            .ConfigureAwait(false))
            while (source.TryReceive(out var item))
                list.Add(item);
        await source.Completion.ConfigureAwait(false);
        return list;
    }

    return Implementation().ContinueWith(t =>
    {
        if (t.IsCanceled) return t;
        Debug.Assert(source.Completion.IsCompleted);
        if (source.Completion.IsFaulted)
        {
            var tcs = new TaskCompletionSource<List<T>>();
            tcs.SetException(source.Completion.Exception.InnerExceptions);
            return tcs.Task;
        }
        return t;
    }, default, TaskContinuationOptions.DenyChildAttach |
        TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default).Unwrap();
}

事實證明,為了滿足我的要求, TPL Dataflow有點矯枉過正。 我能夠使用async/awaitTask.WhenAll來滿足我的要求。 我使用了 Microsoft How-To How to: Extend the async Walkthrough by Using Task.WhenAll (C#)作為模型。

關於我的“問題”

我的“問題”不是問題。 可以將事件方法簽名(在我的情況下,啟動我的搜索的“開始”按鈕單擊方法)修改為async 在 Microsoft How-To GetURLContentsAsync解決方案中,請參閱startButton_Click方法簽名:

private async void startButton_Click(object sender, RoutedEventArgs e)  
{  
    .
    .
}  

關於我的問題

使用 Task.WhenAll,我可以等待所有查詢完成,然后處理所有輸出以在我的 UI 上使用。 在 Microsoft How-To GetURLContentsAsync解決方案中,請參見SumPageSizesAsync方法,即 int 命名lengths數組是所有輸出的總和。

private async Task SumPageSizesAsync()  
{  
    .
    .
    // Create a query.   
    IEnumerable<Task<int>> downloadTasksQuery = from url in urlList select ProcessURLAsync(url);  

    // Use ToArray to execute the query and start the download tasks.  
    Task<int>[] downloadTasks = downloadTasksQuery.ToArray();  

    // Await the completion of all the running tasks.  
    Task<int[]> whenAllTask = Task.WhenAll(downloadTasks);  

    int[] lengths = await whenAllTask;  
    .
    .
}    

正確使用 Dataflow 塊會產生更簡潔和更快的代碼。 數據流塊不是代理或任務。 它們旨在在塊管道中工作,與LinkTo調用連接,而不是手動編碼。

似乎場景是下載一些數據,例如一些 CSV,解析它們並將它們插入數據庫。 這些步驟中的每一個都可以進入自己的塊:

  • 具有 DOP>1 的下載器,以允許同時運行多個下載而不會使網絡泛濫。
  • 將文件轉換為對象數組的解析器
  • 一個使用 SqlBulkCopy 以最快的方式將行批量插入數據庫的導入器,使用最少的日志記錄。
var downloadDOP=8;
var parseDOP=2;
var tableName="SomeTable";

var linkOptions=new DataflowLinkOptions { PropagateCompletion = true};

var downloadOptions =new ExecutionDataflowBlockOptions {
    MaxDegreeOfParallelism = downloadDOP,
};

var parseOptions =new ExecutionDataflowBlockOptions {
    MaxDegreeOfParallelism = parseDOP,
};

使用這些選項,我們可以構建塊管道

//HttpClient is thread-safe and reusable
HttpClient http=new HttpClient(...);

var downloader=new TransformBlock<(Uri,string),FileInfo>(async (uri,path)=>{
    var file=new FileInfo(path);
    using var stream =await httpClient.GetStreamAsync(uri);
    using var fileStream=file.Create();
    await stream.CopyToAsync(stream);
    return file;
},downloadOptions);

var parser=new TransformBlock<FileInfo,Foo[]>(async file=>{
    using var reader = file.OpenText();
    using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
    var records = csv.GetRecords<Foo>().ToList();
    return records;
},parseOptions);

var importer=new ActionBlock<Foo[]>(async recs=>{
    using var bcp=new SqlBulkCopy(connectionString, SqlBulkCopyOptions.TableLock);
    bcp.DestinationTableName=tableName;

    //Map columns if needed
    ...
    using var reader=ObjectReader.Create(recs);
    await bcp.WriteToServerAsync(reader);
});

downloader.LinkTo(parser,linkOptions);
parser.LinkTo(importer,linkOptions);

管道完成后,您可以開始將 Uris 發布到頭塊並等待直到尾塊完成:

IEnumerable<(Uri,string)> filesToDownload = ...


foreach(var pair in filesToDownload)
{
    await downloader.SendAsync(pair);
}

downloader.Complete();

await importer.Completion;

該代碼使用CsvHelper解析 CSV 文件並使用FastMember 的 ObjectReader在 CSV 記錄上創建 IDataReader 包裝器。

在每個塊中,您可以使用Progress實例根據管道的進度更新 UI

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM