我可以在后台运行多个缓慢的进程,以便多个任务可以并行运行吗?

时间:2019-02-05 01:09:05

标签: c# multithreading asynchronous async-await task-parallel-library

我在Core .NET 2.2框架的顶部有一个使用C#编写的控制台应用程序。

我的应用程序允许我使用Windows任务计划程序触发长期运行的管理作业。

其中一个管理员作业会进行Web-API调用,该调用会在将大量文件上传到Azure Blob存储之前下载大量文件。这是我的代码需要完成的逻辑步骤

  1. 调用远程API并以Mime消息作为响应,其中每个消息代表一个文件。
  2. 解析Mime消息并将每条消息转换为MemoryStream,以创建MemoryStream的集合

一旦我有一个包含多个1000+ MemoryStream的集合,我想将每个Stream写入Azure Blob存储。由于写入远程存储的速度很慢,因此我希望可以使用其自己的进程或线程来执行每个写入迭代。这将使我可以同时并行运行潜在的1000多个线程,而不必等待每个写入操作的结果。每个线程将负责记录在写/上载过程中可能发生的任何错误。使用其他作业将处理所有记录的错误,因此我不必担心重试。

我的理解是,调用异步写入/上传流的代码将做到这一点。换句话说,我会说“有一个Stream执行它并一直运行到需要的时间。只要任务完成,我就不在乎结果。”

在进行测试时,我发现我对调用async的理解有些无效。我的印象是,当调用用async定义的方法时,它将在后台线程/工作器中执行,直到该过程完成为止。但是,当我测试代码时,我的理解失败了。我的代码告诉我,如果不添加关键字awaitasync代码将永远不会真正执行。同时,添加关键字await时,代码将等待直到过程完成执行后再继续。换句话说,为我的需要添加await会达到异步调用该方法的目的。

这里是我的代码的精简版,目的是解释我要完成的工作

public async Task Run()
{
    // This gets populated after calling the web-API and parsing out the result
    List<Stream> files = new List<MemoryStream>{.....};

    foreach (Stream file in files)
    {
        // This code should get executed in the background without having to await the result
        await Upload(file);
    }
}

// This method is responsible of upload a stream to a storage and log error if any
private async Task Upload(Stream stream)
{
    try
    {
        await Storage.Create(file, GetUniqueName());
    } 
    catch(Exception e)
    {
        // Log any errors
    }
}

从上述代码中,调用await Upload(file);可以正常工作,并且将按预期方式上传文件。但是,由于调用await方法时使用的是Upload(),因此在上载代码完成之前,我的循环不会跳转到下一个迭代。同时,删除await关键字后,循环不会等待上传过程,但是Stream从未像我从未调用过代码那样实际写入存储。

如何并行执行多个Upload方法,以使每个上载后台运行一个线程?

5 个答案:

答案 0 :(得分:8)

将列表转换为“上传”任务列表,并使用Task.WhenAll()等待所有任务:

public async Task Run()
{
    // This gets populated after calling the web-API and parsing out the result
    List<Stream> files = new List<MemoryStream>{.....};
    var tasks = files.Select(Upload);

    await Task.WhenAll(tasks);
}

有关任务/等待的更多信息,请参见this post

答案 1 :(得分:4)

  

我希望可以使用自己的进程或线程来执行每个写迭代。

这并不是真正做到这一点的最佳方法。进程和线程是有限的资源。您的限制因素正在网络上等待执行操作。

您想要做的只是像这样:

var tasks = new List<Task>(queue.Count);

while (queue.Count > 0)
{
  var myobject = Queue.Dequeue();
  var task = blockBlob.UploadFromByteArrayAsync(myobject.content, 0, myobject.content.Length);
  tasks.Add(task);
}
await Task.WhenAll(tasks);

在这里,我们只是在尽可能快地创建任务,然后等待所有任务完成。我们只让.Net框架负责其余的工作。

这里重要的是线程不会提高等待网络资源的速度。任务是从线程手中委派需要完成的工作的一种方法,因此您有更多的线程可以做任何事情(例如启动新的上传或对完成的上传做出响应)。如果线程只是等待上传完成,那是浪费的资源。

答案 2 :(得分:3)

您可能需要此:

var tasks = files.Select(Upload);
await Task.WhenAll(tasks);

请注意,它将生成与您拥有的文件一样多的任务,如果有太多的文件,则可能会使进程/机器崩溃。请参见Have a set of Tasks with only X running at a time作为解决该问题的示例。

答案 3 :(得分:3)

其他答案很好,但是另一种方法是在https://www.nuget.org/packages/System.Threading.Tasks.Dataflow/

的Nuget中使用您的 TPL DataFlow
public static async Task DoWorkLoads(List<Something> results)
{
   var options = new ExecutionDataflowBlockOptions
                     {
                        MaxDegreeOfParallelism = 50
                     };

   var block = new ActionBlock<Something>(MyMethodAsync, options);

   foreach (var result in results)
      block.Post(result );

   block.Complete();
   await block.Completion;

}

...

public async Task MyMethodAsync(Something result)
{       
   //  Do async work here
}

数据流的优势

  1. async基于任务的解决方案一样,它自然可以与WhenAll一起使用
  2. 它也可以用于更大的任务管道
    • 您可以通过重新输入错误来重试错误。
    • 将所有预处理调用添加到较早的块中
  3. 如果需要节流,则可以限制MaxDegreeOfParallelism
  4. 您可以创建更复杂的管道,因此使用DataFlow的名称

答案 4 :(得分:0)

您可以将代码转换为Azure Function,并让Azure处理大多数并行性,扩展并上传到Azure Blob存储工作。

您可以使用Http触发器或Service Bus触发器来启动每个下载,处理和上传任务。