Parallel.ForEach vs Task.Run và Task.When ALL


158

Sự khác biệt giữa việc sử dụng Parallel.ForEach hoặc Task.Run () để bắt đầu một tập hợp các nhiệm vụ không đồng bộ là gì?

Phiên bản 1:

List<string> strings = new List<string> { "s1", "s2", "s3" };
Parallel.ForEach(strings, s =>
{
    DoSomething(s);
});

Phiên bản 2:

List<string> strings = new List<string> { "s1", "s2", "s3" };
List<Task> Tasks = new List<Task>();
foreach (var s in strings)
{
    Tasks.Add(Task.Run(() => DoSomething(s)));
}
await Task.WhenAll(Tasks);

3
Tôi nghĩ rằng đoạn mã thứ 2 sẽ gần bằng với đoạn thứ nhất nếu bạn sử dụng Task.WaitAllthay vì Task.WhenAll.
avo

15
Cũng xin lưu ý rằng cái thứ hai sẽ thực hiện DoS Something ("s3") ba lần và nó sẽ không tạo ra kết quả tương tự! stackoverflow.com/questions/4684320/ Lời
Nullius


@Dan: lưu ý rằng Phiên bản 2 sử dụng async / await, có nghĩa là đó là một câu hỏi khác. Async / await đã được giới thiệu với VS 2012, 1,5 năm sau khi chủ đề trùng lặp có thể được viết.
Petter T

Câu trả lời:


159

Trong trường hợp này, phương thức thứ hai sẽ chờ đợi không đồng bộ các tác vụ hoàn thành thay vì chặn.

Tuy nhiên, có một bất lợi khi sử dụng Task.Runtrong một vòng lặp - Với Parallel.ForEach, có một Partitionerđiều được tạo ra để tránh thực hiện nhiều nhiệm vụ hơn mức cần thiết. Task.Runsẽ luôn tạo một nhiệm vụ cho mỗi mục (vì bạn đang làm điều này), nhưng các Parallelnhóm lớp hoạt động để bạn tạo ra ít nhiệm vụ hơn tổng số mục công việc. Điều này có thể cung cấp hiệu suất tổng thể tốt hơn đáng kể, đặc biệt nếu thân vòng lặp có khối lượng công việc nhỏ cho mỗi mục.

Nếu đây là trường hợp, bạn có thể kết hợp cả hai tùy chọn bằng cách viết:

await Task.Run(() => Parallel.ForEach(strings, s =>
{
    DoSomething(s);
}));

Lưu ý rằng điều này cũng có thể được viết dưới dạng ngắn hơn:

await Task.Run(() => Parallel.ForEach(strings, DoSomething));

1
Câu trả lời tuyệt vời, tôi đã tự hỏi nếu bạn có thể chỉ cho tôi một tài liệu đọc tốt về chủ đề này?
Dimitar Dimitrov

@DimitarDimitrov Đối với công cụ TPL nói chung, reedcopsey.com/series/metism-in-net4
Reed Copsey

1
Cấu trúc Parallel.ForEach của tôi đã làm hỏng ứng dụng của tôi. Tôi đã thực hiện một số xử lý hình ảnh nặng bên trong nó. Tuy nhiên, khi tôi thêm Task.Run (() => Parallel.ForEach (....)); Nó ngừng đâm. Bạn có thể giải thích lý do tại sao? Xin lưu ý rằng tôi giới hạn các tùy chọn song song với số lượng lõi trên hệ thống.
khỉ nhảy

3
Nếu DoSomethingasync void DoSomethinggì?
Francesco Bonizzi

1
Thế còn async Task DoSomething?
Shawn Mclean

37

Phiên bản đầu tiên sẽ chặn đồng bộ chuỗi cuộc gọi (và chạy một số tác vụ trên nó).
Nếu đó là một giao diện người dùng, điều này sẽ đóng băng giao diện người dùng.

Phiên bản thứ hai sẽ chạy các tác vụ không đồng bộ trong nhóm luồng và giải phóng chuỗi cuộc gọi cho đến khi hoàn thành.

Cũng có sự khác biệt trong các thuật toán lập lịch được sử dụng.

Lưu ý rằng ví dụ thứ hai của bạn có thể được rút ngắn thành

await Task.WhenAll(strings.Select(s => Task.Run(() => DoSomething(s)));

2
không nên nó được await Task.WhenAll(strings.Select(async s => await Task.Run(() => DoSomething(s)));? Tôi gặp vấn đề khi trả lại các nhiệm vụ (thay vì chờ đợi) đặc biệt là khi các câu lệnh như usingcó liên quan để xử lý các đối tượng.
Martín Coll

Cuộc gọi Parallel.ForEach của tôi đã khiến giao diện người dùng của tôi gặp sự cố, tôi đã thêm Task.Run (() => Parallel.ForEach (....)); với nó và nó đã giải quyết sự cố.
khỉ nhảy

0

Tôi đã kết thúc việc này, vì cảm thấy dễ đọc hơn:

  List<Task> x = new List<Task>();
  foreach(var s in myCollectionOfObject)
  {
      // Note there is no await here. Just collection the Tasks
      x.Add(s.DoSomethingAsync());
  }
  await Task.WhenAll(x);

Bằng cách này, bạn đang thực hiện các Nhiệm vụ đang được thực hiện lần lượt hoặc khi Tất cả bắt đầu tất cả chúng cùng một lúc?
Vinicius Gualberto

Theo như tôi có thể nói, tất cả đều bắt đầu khi tôi gọi "DoS SomethingAsync ()". Tuy nhiên, không có gì chặn họ, cho đến khi Khi Tất cả được gọi.
Chris M.

Ý bạn là khi "DoS SomethingAsync ()" đầu tiên được gọi?
Vinicius Gualberto

1
@ChrisM. Nó sẽ bị chặn cho đến khi chờ đợi đầu tiên của DoS SomethingAsync () vì đây là thứ sẽ chuyển thực thi trở lại vòng lặp của bạn. Nếu nó đồng bộ và trả lại một Tác vụ, tất cả các mã sẽ được chạy lần lượt và Khi Tất cả sẽ chờ tất cả các Nhiệm vụ hoàn thành
Simon Belanger

0

Tôi đã thấy Parallel.ForEach được sử dụng không phù hợp và tôi đã tìm ra một ví dụ trong câu hỏi này sẽ giúp ích.

Khi bạn chạy mã bên dưới trong ứng dụng Console, bạn sẽ thấy cách các tác vụ được thực thi trong Parallel.ForEach không chặn chuỗi cuộc gọi. Điều này có thể ổn nếu bạn không quan tâm đến kết quả (tích cực hoặc tiêu cực) nhưng nếu bạn cần kết quả đó, bạn nên đảm bảo sử dụng Task.When ALL.

using System;
using System.Linq;
using System.Threading.Tasks;

namespace ParrellelEachExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var indexes = new int[] { 1, 2, 3 };

            RunExample((prefix) => Parallel.ForEach(indexes, (i) => DoSomethingAsync(i, prefix)),
                "Parallel.Foreach");

            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("*You'll notice the tasks haven't run yet, because the main thread was not blocked*");
            Console.WriteLine("Press any key to start the next example...");
            Console.ReadKey();

            RunExample((prefix) => Task.WhenAll(indexes.Select(i => DoSomethingAsync(i, prefix)).ToArray()).Wait(),
                "Task.WhenAll");
            Console.WriteLine("All tasks are done.  Press any key to close...");
            Console.ReadKey();
        }

        static void RunExample(Action<string> action, string prefix)
        {
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine($"{Environment.NewLine}Starting '{prefix}'...");
            action(prefix);
            Console.WriteLine($"{Environment.NewLine}Finished '{prefix}'{Environment.NewLine}");
        }


        static async Task DoSomethingAsync(int i, string prefix)
        {
            await Task.Delay(i * 1000);
            Console.WriteLine($"Finished: {prefix}[{i}]");
        }
    }
}

Đây là kết quả:

nhập mô tả hình ảnh ở đây

Phần kết luận:

Sử dụng Parallel.ForEach với tác vụ sẽ không chặn chuỗi cuộc gọi. Nếu bạn quan tâm đến kết quả, hãy chắc chắn chờ đợi các nhiệm vụ.

~ Chúc mừng

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.