任务工厂的每个循环等待

时间:2018-12-30 21:06:44

标签: c# asynchronous task task-parallel-library

我是新手,对用法有疑问。 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();
    }
}

6 个答案:

答案 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.ForEachTask.WhenAll,因为这些功能不是为异步流而设计的。

答案 3 :(得分:1)

我的看法是:大多数耗时的操作将使用GET操作获取数据,并使用WriteToServer实际调用SqlBulkCopy。如果您查看该类,您会发现有一个本地异步方法WriteToServerAsync方法(docs here) 。在使用Task.Run自己创建任务之前,请始终使用它们。

http GET调用也是如此。您可以为此使用原生HttpClient.GetAsyncdocs 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.Runthe 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);
    });