Sử dụng async / đang chờ cho nhiều tác vụ


406

Tôi đang sử dụng một ứng dụng khách API hoàn toàn không đồng nhất, nghĩa là, mỗi hoạt động đều trả về Taskhoặc Task<T>, ví dụ:

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

Sử dụng toán tử async / await C # 5, cách chính xác / hiệu quả nhất để bắt đầu nhiều tác vụ và chờ tất cả chúng hoàn thành:

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

hoặc là:

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

Vì ứng dụng khách API đang sử dụng HTTPClient trong nội bộ, tôi mong muốn điều này sẽ phát hành 5 yêu cầu HTTP ngay lập tức, ghi vào bảng điều khiển khi mỗi yêu cầu hoàn tất.


Và vấn đề là gì?
Serg Shevchenko

1
@SergShevchenko Vấn đề là Parallel.ForEach của anh ta được thực hiện một cách không ổn định (xem câu trả lời) - anh ta hỏi liệu nỗ lực của mình để chạy mã async song song có đúng không, đưa ra hai lần thử giải pháp và nếu một cái tốt hơn cái kia (và có lẽ tại sao vậy ).
AnorZaken

Câu trả lời:


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

Mặc dù bạn chạy các hoạt động song song với đoạn mã trên, mã này chặn từng luồng mà mỗi thao tác chạy. Ví dụ: nếu cuộc gọi mạng mất 2 giây, mỗi chuỗi bị treo trong 2 giây sẽ không làm gì ngoài việc chờ đợi.

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

Mặt khác, đoạn mã trên WaitAllcũng chặn các luồng và các luồng của bạn sẽ không được tự do xử lý bất kỳ công việc nào khác cho đến khi hoạt động kết thúc.

Phương pháp đề xuất

Tôi muốn WhenAllđiều đó sẽ thực hiện các hoạt động của bạn không đồng bộ song song.

public async Task DoWork() {

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

Trong thực tế, trong trường hợp trên, bạn thậm chí không cần await, bạn chỉ có thể trực tiếp quay lại từ phương thức vì bạn không có bất kỳ sự tiếp nối nào:

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

Để sao lưu điều này, đây là một bài đăng blog chi tiết đi qua tất cả các lựa chọn thay thế và ưu điểm / nhược điểm của chúng: Cách thức và nơi không đồng bộ I / O không đồng bộ với API Web ASP.NET


31
"đoạn mã trên WaitAllcũng chặn các luồng" - không phải nó chỉ chặn một luồng, chuỗi được gọi WaitAllsao?
Rawling

5
@Rawling tài liệu nói rằng "Loại: System.Threading.T Nhiệm.Task [] Một mảng các trường hợp Nhiệm vụ phải chờ.". Vì vậy, nó chặn tất cả các chủ đề.
Mixxiphoid

30
@Mixxiphoid: Bit bạn trích dẫn không có nghĩa là nó chặn tất cả các luồng. Nó chỉ chặn luồng cuộc gọi trong khi các tác vụ được cung cấp đang chạy. Làm thế nào những nhiệm vụ được thực sự chạy, phụ thuộc vào lịch trình. Thông thường sau khi mỗi tác vụ hoàn thành, luồng mà nó đang chạy sẽ được đưa trở lại nhóm. Mỗi luồng sẽ không bị chặn cho đến khi những người khác hoàn thành.
musaul

3
@tugberk, Theo cách tôi hiểu, sự khác biệt duy nhất giữa các phương thức Tác vụ "cổ điển" và các đối tác Async là cách chúng tương tác với các luồng giữa khi một tác vụ bắt đầu chạy và nó kết thúc chạy. Phương thức cổ điển trong bộ lập lịch mặc định sẽ hog một luồng trong khoảng thời gian đó (ngay cả khi nó đang "ngủ"), trong khi các phương thức không đồng bộ sẽ không. Không có sự khác biệt ngoài thời gian đó, tức là nhiệm vụ là lịch trình nhưng chưa bắt đầu và khi nó đã hoàn thành nhưng người gọi vẫn đang chờ.
musaul

3
@tugberk Xem stackoverflow.com/a/6123432/750216 sự khác biệt nằm ở việc liệu cuộc gọi có bị chặn hay không, phần còn lại là như nhau. Bạn có thể muốn chỉnh sửa câu trả lời để làm rõ.
Gấu trúc Răzvan Flavius

45

Tôi tò mò muốn xem kết quả của các phương pháp được cung cấp trong câu hỏi cũng như câu trả lời được chấp nhận, vì vậy tôi đã đưa nó vào thử nghiệm.

Đây là mã:

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

Và kết quả đầu ra:

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.

2
Nếu bạn đặt thời gian cho mỗi kết quả này, điều này sẽ hữu ích hơn
Serj Sagan

8
@SerjSagan Ý tưởng ban đầu của tôi chỉ là xác minh rằng các công nhân đang được bắt đầu đồng thời trong từng trường hợp, nhưng tôi đã thêm dấu thời gian để cải thiện sự rõ ràng của bài kiểm tra. Cám ơn vì sự gợi ý.
RiaanDP

Cảm ơn bạn đã kiểm tra. Tuy nhiên, có một chút kỳ lạ khi bạn đang chạy thread.s ngủ trên một luồng tách biệt với "thread worker". Không phải là vấn đề trong trường hợp này, nhưng nó sẽ không có ý nghĩa hơn với Nhiệm vụ. Hãy xử lý các luồng công nhân nếu chúng ta đang mô phỏng công việc tính toán, hoặc chỉ là Nhiệm vụ. Thay vì ngủ nếu chúng ta đang mô phỏng i / o? Chỉ cần kiểm tra suy nghĩ của bạn về điều đó.
AnorZaken

24

Vì API bạn đang gọi là không đồng bộ, Parallel.ForEachphiên bản không có ý nghĩa nhiều. Bạn không nên sử dụng .Waittrong WaitAllphiên bản vì điều đó sẽ làm mất tính song song Một cách khác nếu người gọi không đồng bộ đang sử dụng Task.WhenAllsau khi thực hiện SelectToArrayđể tạo ra các mảng tác vụ. Một thay thế thứ hai là sử dụng Rx 2.0


10

Bạn có thể sử dụng Task.WhenAllchức năng mà bạn có thể vượt qua n nhiệm vụ; Task.WhenAllsẽ trả về một nhiệm vụ chạy đến khi hoàn thành khi tất cả các nhiệm vụ mà bạn đã Task.WhenAllhoàn thành. Bạn phải chờ không đồng bộ Task.WhenAllđể bạn không chặn luồng UI của mình:

   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
   }

8

Parallel.ForEachyêu cầu một danh sách các công nhân do người dùng định nghĩakhông đồng bộ Action để thực hiện với mỗi công nhân.

Task.WaitAllTask.WhenAllyêu cầu một List<Task>, theo định nghĩa không đồng bộ.

Tôi thấy phản hồi của RiaanDP rất hữu ích để hiểu sự khác biệt, nhưng nó cần một sự điều chỉnh cho . Không đủ danh tiếng để trả lời bình luận của anh ấy, do đó phản ứng của riêng tôi.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)));
        }
    }
}

Đầu ra kết quả là dưới đây. Thời gian thực hiện là tương đương. Tôi đã chạy thử nghiệm này trong khi máy tính của tôi đang thực hiện quét chống vi-rút hàng tuần. Thay đổi thứ tự của các bài kiểm tra đã thay đổi thời gian thực hiện trên chúng.

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.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.