[英]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.ForEach
和Task.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.