我是新手,对用法有疑问。 Task.Factory是否会在foreach循环中或在“等待”时阻塞所有项目,从而基本上使程序成为单线程?如果我正确地考虑了这个问题,则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();
}
}
答案 0 :(得分:2)
您正在等待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个线程
如果您删除await
前面的StartNew
,则会看到它并行运行。正如其他人提到的那样,您当然可以使用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
答案 1 :(得分:1)
您需要将每个任务添加到集合中,然后使用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());
}
答案 2 :(得分:1)
这是C# 8.0 Async Streams即将解决的典型问题。
在C#8.0发布之前,您可以使用AsyncEnumarator library:
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
,因为这些功能不是为异步流而设计的。
答案 3 :(得分:1)
我的看法是:大多数耗时的操作将使用GET操作获取数据,并使用WriteToServer
实际调用SqlBulkCopy
。如果您查看该类,您会发现有一个本地异步方法WriteToServerAsync
方法(docs here)
。在使用Task.Run
自己创建任务之前,请始终使用它们。
http GET调用也是如此。您可以为此使用原生HttpClient.GetAsync
(docs here)。
这样做可以将代码重写为此:
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服务器调用)都是I / O调用。使用本地异步/等待模式甚至不会创建或使用线程,请参阅this question以获得更深入的说明。因此,对于IO绑定操作,您永远不必使用Task.Run
(或Task.Factory.StartNew
。但是请注意,Task.Run
是the recommended approach)。
旁注:如果您循环使用HttpClient
,请阅读this,了解如何正确使用它。
如果您需要限制并行操作的数量,您还可以使用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;
}
答案 4 :(得分:0)
为什么不尝试这样做:),该程序将不会启动并行任务(在foreach中),它将被阻塞,但任务中的逻辑将在与线程池不同的线程中完成(当时仅一个线程,但是主线程将被阻止。)
您所处的正确方法是使用Paraller.ForEach How can I convert this foreach code to Parallel.ForEach?
答案 5 :(得分:0)
使用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);
});