ChatGPT解决这个技术问题 Extra ChatGPT

对多个任务使用 async/await

我正在使用一个完全异步的 API 客户端,即每个操作都返回 TaskTask<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);
}

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

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

或者:

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

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

问题是什么?
@SergShevchenko问题是他的 Parallel.ForEach 完成不正确(请参阅答案)-他询问他并行运行异步代码的尝试是否正确,提供了两种解决方案尝试,以及一种是否比另一种更好(大概为什么会这样) )。

t
tugberk
int[] ids = new[] { 1, 2, 3, 4, 5 };
Parallel.ForEach(ids, i => DoSomething(1, i, blogClient).Wait());

尽管您与上述代码并行运行这些操作,但此代码会阻止每个操作运行的每个线程。例如,如果网络调用需要 2 秒,则每个线程会挂起 2 秒,除了等待之外什么都不做。

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

另一方面,上面带有 WaitAll 的代码也会阻塞线程,并且在操作结束之前,您的线程将无法自由处理任何其他工作。

推荐方法

我更喜欢 WhenAll,它将以并行方式异步执行您的操作。

public async Task DoWork() {

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

实际上,在上述情况下,您甚至不需要等待,您可以直接从方法中返回,因为您没有任何延续: public Task DoWork() { int[] ids = new[] { 1 , 2, 3, 4, 5 }; return Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient))); }

为了支持这一点,这里有一篇详细的博文,介绍了所有备选方案及其优缺点:How and Where Concurrent Asynchronous I/O with ASP.NET Web API


“上面带有 WaitAll 的代码也阻塞了线程” - 它不是只阻塞 一个 线程,即调用 WaitAll 的那个吗?
@Rawling documentation 声明“类型:System.Threading.Tasks.Task[] 要等待的任务实例数组。”。因此,它阻塞了所有线程。
@Mixxiphoid:您引用的位并不意味着它会阻塞所有线程。它仅在提供的任务运行时阻塞调用线程。这些任务的实际运行方式取决于调度程序。通常在每个任务完成后,它正在运行的线程将返回到池中。在其他线程完成之前,每个线程都不会保持阻塞状态。
@tugberk,按照我的理解,“经典”任务方法和异步对应方法之间的唯一区别是它们在任务开始运行和完成运行之间如何与线程交互。默认调度程序下的经典方法将在此期间占用一个线程(即使它是“睡眠”),而异步方法则不会。在那段时间之外没有区别,即任务是计划但尚未开始,当它完成但它的调用者仍在等待。
@tugberk 参见stackoverflow.com/a/6123432/750216,区别在于调用线程是否被阻塞,其余相同。您可能需要编辑答案以澄清。
B
Bogdan Stăncescu

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

这是代码:

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)));
        }
    }
}

结果输出:

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.

如果您将时间放在每个结果上,这将更有用
@SerjSagan 我最初的想法只是验证在每种情况下工作人员是否同时启动,但我添加了时间戳以提高测试的清晰度。谢谢你的建议。
谢谢你的测试。但是,您在与“工作线程”分开的线程上运行 thread.sleep 感觉有点奇怪。在这种情况下并不重要,但是如果我们正在模拟计算工作,Task.Run 工作线程是否更有意义,或者如果我们正在模拟 i/o,则只使用 Task.Delay 而不是 sleep?只是检查您对此的想法。
L
Liam

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


K
KreepN

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

   public async Task DoSomeThing() {
       
       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
   }

J
JPortillo

Parallel.ForEach 需要一个用户定义 工作人员列表和一个非异步 Action 才能与每个工作人员一起执行。

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

我发现 RiaanDPresponse 对于理解差异非常有用,但需要对 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)));
        }
    }
}

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

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.

这是最有趣的。从并行工作人员中删除“.Wait()”,使它们以与其他工作相同的速度运行。同样, PerformTest_ParallelForEach 不会很快返回 - 它仍在等待工作人员完成。不知道为什么,当 Parallel 使用 5 个单独的线程时,它们不会全部并行运行并在一秒钟内完成。我错过了什么?
之前的响应和我的不同之处在于我将不同的工作类型传递给迭代器。我将 DoWork(非 async Action)传递给 Parallel.ForEach,将 DoWorkAsyncasync Task)传递给 Task.WaitAllTask.WhenAllParallel.ForEach 需要 Task。将 .Wait() 添加到 DoWorkAsync 使其成为 Task,但这会阻止并发,这不是我们想要的。
干杯是有道理的。我的错,它们确实在 Parallel.ForEach 中并排运行,但它们具有不同的 SleepTimeout 值。错过了。
F
Feng Jiang

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

以下代码适用于调用不同的函数。只需将您的常规 Task.Run() 放入一个数组并使用 Task.WhenAll() 调用:

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

this answerthis answer 比您在这里所做的更“复杂”吗? awaitTask.WhenAll() 一样。
Y
Ygalbel

我只想补充以上所有出色的答案,如果您编写了一个库,那么使用 ConfigureAwait(false) 并获得更好的性能是一个很好的做法,如 here 所述。

所以这个片段似乎更好:

 public static async Task DoWork() 
 {
     int[] ids = new[] { 1, 2, 3, 4, 5 };
     await Task.WhenAll(ids.Select(i => DoSomething(1, i))).ConfigureAwait(false);
 }

完整的小提琴链接 here


这是正确的,但我怀疑 OP 正在编写一个库。看起来他们正在编写应用程序代码的可能性更大,其中 ConfigureAwait 只是使代码混乱并妨碍了工作,实际上没有提供任何回报(性能方面)。
你说得对,但我认为这是本次讨论中的重要评论。