簡體   English   中英

帶有等待的每個循環的任務工廠

[英]Task Factory for each loop with await

我是新手,對使用有疑問。 Task.Factory 是否為 foreach 循環中的所有項目觸發,或者在“await”處阻塞,基本上使程序成為單線程的? 如果我正確考慮這一點,foreach 循環將啟動所有任務和 .GetAwaiter().GetResult(); 阻塞主線程直到最后一個任務完成。

另外,我只是想要一些匿名任務來加載數據。 這會是一個正確的實現嗎? 我不是指異常處理,因為這只是一個例子。

為了清楚起見,我正在從外部 API 將數據加載到數據庫中。 這是使用 FRED 數據庫。 https://fred.stlouisfed.org/ ),但我有幾個我將完成整個傳輸(可能是 200k 數據點)。 完成后,我更新表格,刷新市場計算等。有些是實時的,有些是日終的。 我還想說,我目前所有的東西都在 docker 中工作,但一直在努力使用任務更新代碼以改進執行。

class Program
{
    private async Task SQLBulkLoader() 
    {

        foreach (var fileListObj in indicators.file_list)
        {
            await Task.Factory.StartNew(  () =>
            {

                string json = this.GET(//API call);

                SeriesObject obj = JsonConvert.DeserializeObject<SeriesObject>(json);

                DataTable dataTableConversion = ConvertToDataTable(obj.observations);
                dataTableConversion.TableName = fileListObj.series_id;

                using (SqlConnection dbConnection = new SqlConnection("SQL Connection"))
                {
                    dbConnection.Open();
                    using (SqlBulkCopy s = new SqlBulkCopy(dbConnection))
                    {
                      s.DestinationTableName = dataTableConversion.TableName;
                      foreach (var column in dataTableConversion.Columns)
                          s.ColumnMappings.Add(column.ToString(), column.ToString());
                      s.WriteToServer(dataTableConversion);
                    }

                  Console.WriteLine("File: {0} Complete", fileListObj.series_id);
                }
             });
        }            
    }

    static void Main(string[] args)
    {
        Program worker = new Program();
        worker.SQLBulkLoader().GetAwaiter().GetResult();
    }
}

您等待從Task.Factory.StartNew返回的任務確實使它成為有效的單線程。 您可以通過這個簡短的 LinqPad 示例看到一個簡單的演示:

for (var i = 0; i < 3; i++)
{
    var index = i;
    $"{index} inline".Dump();
    await Task.Run(() =>
    {
        Thread.Sleep((3 - index) * 1000);
        $"{index} in thread".Dump();
    });
}

在這里,隨着循環的進行,我們等待的時間會減少。 輸出是:

0 內聯
0 線程
1 個內聯
1個線程
2 內聯
2個線程

如果刪除StartNew前面的await ,您將看到它並行運行。 正如其他人所提到的,您當然可以使用Parallel.ForEach ,但為了演示更多手動操作,您可以考慮這樣的解決方案:

var tasks = new List<Task>();

for (var i = 0; i < 3; i++) 
{
    var index = i;
    $"{index} inline".Dump();
    tasks.Add(Task.Factory.StartNew(() =>
    {
        Thread.Sleep((3 - index) * 1000);
        $"{index} in thread".Dump();
    }));
}

Task.WaitAll(tasks.ToArray());

現在注意結果如何:

0 內聯
1 個內聯
2 內聯
2個線程
1個線程
0 線程

您需要將每個任務添加到一個集合中,然后使用Task.WhenAll來等待該集合中的所有任務:

private async Task SQLBulkLoader() 
{ 
  var tasks = new List<Task>();
  foreach (var fileListObj in indicators.file_list)
  {
    tasks.Add(Task.Factory.StartNew( () => { //Doing Stuff }));
  }

  await Task.WhenAll(tasks.ToArray());
}

這是C# 8.0 Async Streams將很快解決的典型問題。

在 C# 8.0 發布之前,您可以使用AsyncEnumerator 庫

using System.Collections.Async;

class Program
{
    private async Task SQLBulkLoader() {

        await indicators.file_list.ParallelForEachAsync(async fileListObj =>
        {
            ...
            await s.WriteToServerAsync(dataTableConversion);
            ...
        },
        maxDegreeOfParalellism: 3,
        cancellationToken: default);
    }

    static void Main(string[] args)
    {
        Program worker = new Program();
        worker.SQLBulkLoader().GetAwaiter().GetResult();
    }
}

我不建議使用Parallel.ForEachTask.WhenAll因為這些函數不是為異步流設計的。

我的看法是:最耗時的操作是使用 GET 操作獲取數據,並使用SqlBulkCopy實際調用WriteToServer 如果您查看該類,您將看到有一個本地異步方法WriteToServerAsync方法( 此處為文檔)。 在使用Task.Run自己創建任務之前,請始終使用它們。

這同樣適用於 http GET 調用。 為此,您可以使用本機HttpClient.GetAsync此處的文檔)。

這樣做,您可以將代碼重寫為:

private async Task ProcessFileAsync(string series_id)
{
    string json = await GetAsync();

    SeriesObject obj = JsonConvert.DeserializeObject<SeriesObject>(json);

    DataTable dataTableConversion = ConvertToDataTable(obj.observations);
    dataTableConversion.TableName = series_id;

    using (SqlConnection dbConnection = new SqlConnection("SQL Connection"))
    {
        dbConnection.Open();
        using (SqlBulkCopy s = new SqlBulkCopy(dbConnection))
        {
            s.DestinationTableName = dataTableConversion.TableName;
            foreach (var column in dataTableConversion.Columns)
                s.ColumnMappings.Add(column.ToString(), column.ToString());
            await s.WriteToServerAsync(dataTableConversion);
        }

        Console.WriteLine("File: {0} Complete", series_id);
    }
}

private async Task SQLBulkLoaderAsync()
{
    var tasks = indicators.file_list.Select(f => ProcessFileAsync(f.series_id));
    await Task.WhenAll(tasks);
}

這兩個操作(http 調用和 sql server 調用)都是 I/O 調用。 使用本機 async/await 模式甚至不會創建或使用線程,請參閱此問題以獲得更深入的解釋。 這就是為什么對於 IO 綁定操作,您永遠不必使用Task.Run (或Task.Factory.StartNew 。但請注意Task.Run推薦的方法)。

旁注:如果您在循環中使用HttpClient ,請閱讀 本文以了解如何正確使用它。

如果您需要限制並行操作的數量,您還可以使用TPL Dataflow,因為它非常適合基於任務的 IO 綁定操作。 然后SQLBulkLoaderAsync修改為(保留本答案之前的ProcessFileAsync方法不變):

private async Task SQLBulkLoaderAsync()
{
    var ab = new ActionBlock<string>(ProcessFileAsync, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 5 });

    foreach (var file in indicators.file_list)
    {
        ab.Post(file.series_id);
    }

    ab.Complete();
    await ab.Completion;
}

你為什么不試試這個:),這個程序不會啟動並行任務(在 foreach 中),它會阻塞但任務中的邏輯將在與線程池不同的線程中完成(當時只有一個,但主線程會被屏蔽)。

在您的情況下,正確的方法是使用 Paraller.ForEach 如何將此 foreach 代碼轉換為 Parallel.ForEach?

使用Parallel.ForEach循環在任何System.Collections.Generic.IEnumerable<T>源上啟用數據並行性。

// Method signature: Parallel.ForEach(IEnumerable<TSource> source, Action<TSource> body)
    Parallel.ForEach(fileList, (currentFile) => 
    {

       //Doing Stuff

      Console.WriteLine("Processing {0} on thread {1}", currentFile, Thread.CurrentThread.ManagedThreadId);
    });

暫無
暫無

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

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