使用async/await执行多个任务

Ben*_*ter 379 .net c# task-parallel-library async-await c#-5.0

我正在使用完全异步的API客户端,也就是说,每个操作都返回Task或者Task<T>,例如:

static async Task DoSomething(int siteId, int postId, IBlogClient client)
{
    await client.DeletePost(siteId, postId); // call API client
    Console.WriteLine("Deleted post {0}.", siteId);
}
Run Code Online (Sandbox Code Playgroud)

使用C#5 async/await运算符,启动多个任务并等待它们全部完成的正确/最有效方法是什么:

int[] ids = new[] { 1, 2, 3, 4, 5 };
Parallel.ForEach(ids, i => DoSomething(1, i, blogClient).Wait());
Run Code Online (Sandbox Code Playgroud)

要么:

int[] ids = new[] { 1, 2, 3, 4, 5 };
Task.WaitAll(ids.Select(i => DoSomething(1, i, blogClient)).ToArray());
Run Code Online (Sandbox Code Playgroud)

由于API客户端在内部使用HttpClient,我希望这会立即发出5个HTTP请求,并在每个请求完成时写入控制台.

tug*_*erk 540

int[] ids = new[] { 1, 2, 3, 4, 5 };
Parallel.ForEach(ids, i => DoSomething(1, i, blogClient).Wait());
Run Code Online (Sandbox Code Playgroud)

虽然您与上面的代码并行运行操作,但此代码会阻止每个操作运行的每个线程.例如,如果网络呼叫需要2秒钟,则每个线程会挂起2秒钟而不做任何事情,只能等待.

int[] ids = new[] { 1, 2, 3, 4, 5 };
Task.WaitAll(ids.Select(i => DoSomething(1, i, blogClient)).ToArray());
Run Code Online (Sandbox Code Playgroud)

另一方面,上面的代码WaitAll也会阻塞线程,你的线程将无法自由处理任何其他工作,直到操作结束.

推荐方法

我更喜欢WhenAll哪个会在Parallel中异步执行你的操作.

public async Task DoWork() {

    int[] ids = new[] { 1, 2, 3, 4, 5 };
    await Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient)));
}
Run Code Online (Sandbox Code Playgroud)

事实上,在上面的例子中,你甚至不需要await,你可以直接从方法返回,因为你没有任何延续:

public Task DoWork() 
{
    int[] ids = new[] { 1, 2, 3, 4, 5 };
    return Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient)));
}
Run Code Online (Sandbox Code Playgroud)

为了支持这一点,这里有一篇详细的博客文章,介绍了所有替代方案及其优点/缺点:ASP.NET Web API的并发异步I/O方式和位置

  • "上面带有`WaitAll`的代码也会阻塞线程" - 它不会阻止*一个*线程,一个叫`WaitAll`的线程? (28认同)
  • @Mixxiphoid:你引用的位并不意味着它阻止所有线程.它在提供的任务运行时仅阻塞调用线程.这些任务的实际运行方式取决于调度程序.通常在每个任务完成后,运行它的线程将返回到池中.在其他线程完成之前,每个线程都不会保持阻塞. (27认同)
  • @Rawling [文档](https://msdn.microsoft.com/en-us/library/dd270695(v = vs.110).aspx)声明"Type:System.Threading.Tasks.Task []一个数组要等待的Task实例." 所以,它会阻止所有线程. (5认同)
  • @tugberk,我理解它的方式,"经典"任务方法和异步对应方法之间的唯一区别是它们在任务开始运行和完成运行之间如何与线程交互.默认调度程序下的经典方法将在该期间内占用一个线程(即使它正在"休眠"),而异步调度程序则不会.在那段时间之外没有区别,即任务是计划但没有开始,当它完成但是它的呼叫者仍然在等待. (3认同)
  • @tugberk请参阅http://stackoverflow.com/a/6123432/750216,区别在于调用线程是否被阻塞,其余是相同的.您可能想要编辑答案以澄清. (3认同)
  • 为了消除猜测,我浏览了[源代码](https://github.com/microsoft/referencesource/blob/master/mscorlib/system/threading/Tasks/Task.cs#L4893)。基本上,“Task.WaitAll()”的实现实现了超时和取消 - 因此,如果您不需要这些功能中的任何一个,可能更喜欢“await Task.WhenAll()”,它要简单得多,并且可以说“ async` 关键字使得在 `async` 方法的上下文中比阻塞方法调用更容易阅读/理解。 (2认同)

Ria*_*nDP 40

我很想知道问题中提供的方法的结果以及接受的答案,所以我对它进行了测试.

这是代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncTest
{
    class Program
    {
        class Worker
        {
            public int Id;
            public int SleepTimeout;

            public async Task DoWork(DateTime testStart)
            {
                var workerStart = DateTime.Now;
                Console.WriteLine("Worker {0} started on thread {1}, beginning {2} seconds after test start.",
                    Id, Thread.CurrentThread.ManagedThreadId, (workerStart-testStart).TotalSeconds.ToString("F2"));
                await Task.Run(() => Thread.Sleep(SleepTimeout));
                var workerEnd = DateTime.Now;
                Console.WriteLine("Worker {0} stopped; the worker took {1} seconds, and it finished {2} seconds after the test start.",
                   Id, (workerEnd-workerStart).TotalSeconds.ToString("F2"), (workerEnd-testStart).TotalSeconds.ToString("F2"));
            }
        }

        static void Main(string[] args)
        {
            var workers = new List<Worker>
            {
                new Worker { Id = 1, SleepTimeout = 1000 },
                new Worker { Id = 2, SleepTimeout = 2000 },
                new Worker { Id = 3, SleepTimeout = 3000 },
                new Worker { Id = 4, SleepTimeout = 4000 },
                new Worker { Id = 5, SleepTimeout = 5000 },
            };

            var startTime = DateTime.Now;
            Console.WriteLine("Starting test: Parallel.ForEach...");
            PerformTest_ParallelForEach(workers, startTime);
            var endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            startTime = DateTime.Now;
            Console.WriteLine("Starting test: Task.WaitAll...");
            PerformTest_TaskWaitAll(workers, startTime);
            endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            startTime = DateTime.Now;
            Console.WriteLine("Starting test: Task.WhenAll...");
            var task = PerformTest_TaskWhenAll(workers, startTime);
            task.Wait();
            endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            Console.ReadKey();
        }

        static void PerformTest_ParallelForEach(List<Worker> workers, DateTime testStart)
        {
            Parallel.ForEach(workers, worker => worker.DoWork(testStart).Wait());
        }

        static void PerformTest_TaskWaitAll(List<Worker> workers, DateTime testStart)
        {
            Task.WaitAll(workers.Select(worker => worker.DoWork(testStart)).ToArray());
        }

        static Task PerformTest_TaskWhenAll(List<Worker> workers, DateTime testStart)
        {
            return Task.WhenAll(workers.Select(worker => worker.DoWork(testStart)));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

结果输出:

Starting test: Parallel.ForEach...
Worker 1 started on thread 1, beginning 0.21 seconds after test start.
Worker 4 started on thread 5, beginning 0.21 seconds after test start.
Worker 2 started on thread 3, beginning 0.21 seconds after test start.
Worker 5 started on thread 6, beginning 0.21 seconds after test start.
Worker 3 started on thread 4, beginning 0.21 seconds after test start.
Worker 1 stopped; the worker took 1.90 seconds, and it finished 2.11 seconds after the test start.
Worker 2 stopped; the worker took 3.89 seconds, and it finished 4.10 seconds after the test start.
Worker 3 stopped; the worker took 5.89 seconds, and it finished 6.10 seconds after the test start.
Worker 4 stopped; the worker took 5.90 seconds, and it finished 6.11 seconds after the test start.
Worker 5 stopped; the worker took 8.89 seconds, and it finished 9.10 seconds after the test start.
Test finished after 9.10 seconds.

Starting test: Task.WaitAll...
Worker 1 started on thread 1, beginning 0.01 seconds after test start.
Worker 2 started on thread 1, beginning 0.01 seconds after test start.
Worker 3 started on thread 1, beginning 0.01 seconds after test start.
Worker 4 started on thread 1, beginning 0.01 seconds after test start.
Worker 5 started on thread 1, beginning 0.01 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.01 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.01 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.01 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.01 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.01 seconds after the test start.
Test finished after 5.01 seconds.

Starting test: Task.WhenAll...
Worker 1 started on thread 1, beginning 0.00 seconds after test start.
Worker 2 started on thread 1, beginning 0.00 seconds after test start.
Worker 3 started on thread 1, beginning 0.00 seconds after test start.
Worker 4 started on thread 1, beginning 0.00 seconds after test start.
Worker 5 started on thread 1, beginning 0.00 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.00 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.00 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.00 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.00 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.00 seconds after the test start.
Test finished after 5.00 seconds.
Run Code Online (Sandbox Code Playgroud)

  • @SerjSagan我最初的想法只是为了验证工作人员在每种情况下同时启动,但我添加了时间戳以提高测试的清晰度.谢谢你的建议. (7认同)
  • 如果您花时间在这些结果中的每一个上,这将更有用 (2认同)

Jam*_*ing 22

由于您调用的API是异步的,因此该Parallel.ForEach版本没有多大意义.你不应该.WaitWaitAll版本中使用,因为这将失去并行性如果调用者是异步的另一种选择是Task.WhenAll在执行后使用SelectToArray生成任务数组.第二种选择是使用Rx 2.0


Ale*_*lov 9

这个问题已有 10 年历史了,OP 正在询问 C# 5。

截至今天,还有一种选择:Parallel.ForEachAsync.NET 6 中引入的方法。

这是一个基于OP代码的示例:

int[] ids = new[] { 1, 2, 3, 4, 5 };
await Parallel.ForEachAsync(ids, async (i,token) 
                                  => await DoSomething(1, i, blogClient));
Run Code Online (Sandbox Code Playgroud)

这是完全异步的,不会阻塞任何线程。

另外,这样可能会更好

  • Task.WaitAll
  • Task.WhenAll

方法,因为它们不限制并行运行的线程数量。因此,如果你有一个巨大的数组,它可能会耗尽你所有的内存。Parallel.ForEachAsync允许您像这样指定并行度:

var options = new ParallelOptions { MaxDegreeOfParallelism = 4 };
await Parallel.ForEachAsync(ids, options, async (i,token) 
                                          => await DoSomething(1, i, blogClient));
Run Code Online (Sandbox Code Playgroud)

这样你就只有 4 个线程并行运行。


小智 7

您可以使用Task.WhenAll可以传递n个任务的函数;Task.WhenAll当您传递完成的所有任务都将完成时,它将返回一个运行完毕的任务Task.WhenAll。您必须异步等待,Task.WhenAll以免阻塞UI线程:

   public async Task DoSomeThing() {

       var Task[] tasks = new Task[numTasks];
       for(int i = 0; i < numTask; i++)
       {
          tasks[i] = CallSomeAsync();
       }
       await Task.WhenAll(tasks);
       // code that'll execute on UI thread
   }
Run Code Online (Sandbox Code Playgroud)


小智 7

Parallel.ForEach需要一个用户定义的工作程序列表和一个与每个工作程序一起执行的非异步程序 Action

Task.WaitAllTask.WhenAll要求List<Task>,根据定义,它们是异步的。

我发现RiaanDP响应对于理解差异非常有用,但是需要对进行更正Parallel.ForEach。没有足够的声誉来回应他的评论,因此是我自己的回应。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncTest
{
    class Program
    {
        class Worker
        {
            public int Id;
            public int SleepTimeout;

            public void DoWork(DateTime testStart)
            {
                var workerStart = DateTime.Now;
                Console.WriteLine("Worker {0} started on thread {1}, beginning {2} seconds after test start.",
                    Id, Thread.CurrentThread.ManagedThreadId, (workerStart - testStart).TotalSeconds.ToString("F2"));
                Thread.Sleep(SleepTimeout);
                var workerEnd = DateTime.Now;
                Console.WriteLine("Worker {0} stopped; the worker took {1} seconds, and it finished {2} seconds after the test start.",
                   Id, (workerEnd - workerStart).TotalSeconds.ToString("F2"), (workerEnd - testStart).TotalSeconds.ToString("F2"));
            }

            public async Task DoWorkAsync(DateTime testStart)
            {
                var workerStart = DateTime.Now;
                Console.WriteLine("Worker {0} started on thread {1}, beginning {2} seconds after test start.",
                    Id, Thread.CurrentThread.ManagedThreadId, (workerStart - testStart).TotalSeconds.ToString("F2"));
                await Task.Run(() => Thread.Sleep(SleepTimeout));
                var workerEnd = DateTime.Now;
                Console.WriteLine("Worker {0} stopped; the worker took {1} seconds, and it finished {2} seconds after the test start.",
                   Id, (workerEnd - workerStart).TotalSeconds.ToString("F2"), (workerEnd - testStart).TotalSeconds.ToString("F2"));
            }
        }

        static void Main(string[] args)
        {
            var workers = new List<Worker>
            {
                new Worker { Id = 1, SleepTimeout = 1000 },
                new Worker { Id = 2, SleepTimeout = 2000 },
                new Worker { Id = 3, SleepTimeout = 3000 },
                new Worker { Id = 4, SleepTimeout = 4000 },
                new Worker { Id = 5, SleepTimeout = 5000 },
            };

            var startTime = DateTime.Now;
            Console.WriteLine("Starting test: Parallel.ForEach...");
            PerformTest_ParallelForEach(workers, startTime);
            var endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            startTime = DateTime.Now;
            Console.WriteLine("Starting test: Task.WaitAll...");
            PerformTest_TaskWaitAll(workers, startTime);
            endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            startTime = DateTime.Now;
            Console.WriteLine("Starting test: Task.WhenAll...");
            var task = PerformTest_TaskWhenAll(workers, startTime);
            task.Wait();
            endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            Console.ReadKey();
        }

        static void PerformTest_ParallelForEach(List<Worker> workers, DateTime testStart)
        {
            Parallel.ForEach(workers, worker => worker.DoWork(testStart));
        }

        static void PerformTest_TaskWaitAll(List<Worker> workers, DateTime testStart)
        {
            Task.WaitAll(workers.Select(worker => worker.DoWorkAsync(testStart)).ToArray());
        }

        static Task PerformTest_TaskWhenAll(List<Worker> workers, DateTime testStart)
        {
            return Task.WhenAll(workers.Select(worker => worker.DoWorkAsync(testStart)));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

结果输出如下。执行时间是可比的。我的计算机每周进行一次防病毒扫描时,我进行了此测试。改变测试的顺序确实改变了它们的执行时间。

Starting test: Parallel.ForEach...
Worker 1 started on thread 9, beginning 0.02 seconds after test start.
Worker 2 started on thread 10, beginning 0.02 seconds after test start.
Worker 3 started on thread 11, beginning 0.02 seconds after test start.
Worker 4 started on thread 13, beginning 0.03 seconds after test start.
Worker 5 started on thread 14, beginning 0.03 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.02 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.02 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.03 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.03 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.03 seconds after the test start.
Test finished after 5.03 seconds.

Starting test: Task.WaitAll...
Worker 1 started on thread 9, beginning 0.00 seconds after test start.
Worker 2 started on thread 9, beginning 0.00 seconds after test start.
Worker 3 started on thread 9, beginning 0.00 seconds after test start.
Worker 4 started on thread 9, beginning 0.00 seconds after test start.
Worker 5 started on thread 9, beginning 0.01 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.01 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.01 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.01 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.01 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.01 seconds after the test start.
Test finished after 5.01 seconds.

Starting test: Task.WhenAll...
Worker 1 started on thread 9, beginning 0.00 seconds after test start.
Worker 2 started on thread 9, beginning 0.00 seconds after test start.
Worker 3 started on thread 9, beginning 0.00 seconds after test start.
Worker 4 started on thread 9, beginning 0.00 seconds after test start.
Worker 5 started on thread 9, beginning 0.00 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.00 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.00 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.00 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.00 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.01 seconds after the test start.
Test finished after 5.01 seconds.
Run Code Online (Sandbox Code Playgroud)


Jfl*_*fly 5

所有答案都是为了运行相同的功能。

以下代码适用于调用不同的函数。只需将常规放入Task.Run()数组中并使用以下命令进行调用即可Task.WhenAll()

await Task.WhenAll(new Task[] { 
    Task.Run(() => Func1(args)),
    Task.Run(() => Func2(args))
});
Run Code Online (Sandbox Code Playgroud)