Parallel.ForEach vs Task.Factory.StartNew


267

Sự khác biệt giữa các đoạn mã dưới đây là gì? Cả hai sẽ không sử dụng chủ đề threadpool?

Chẳng hạn, nếu tôi muốn gọi một hàm cho mỗi mục trong một bộ sưu tập,

Parallel.ForEach<Item>(items, item => DoSomething(item));

vs

foreach(var item in items)
{
  Task.Factory.StartNew(() => DoSomething(item));
}

Câu trả lời:


302

Đầu tiên là một lựa chọn tốt hơn nhiều.

Parallel.ForEach, trong nội bộ, sử dụng a Partitioner<T>để phân phối bộ sưu tập của bạn vào các mục công việc. Nó sẽ không thực hiện một nhiệm vụ cho mỗi mục, mà thay vào đó là đợt này để giảm chi phí liên quan.

Tùy chọn thứ hai sẽ lên lịch Taskcho mỗi mục trong bộ sưu tập của bạn. Mặc dù các kết quả sẽ (gần như) giống nhau, nhưng điều này sẽ giới thiệu nhiều chi phí hơn mức cần thiết, đặc biệt là đối với các bộ sưu tập lớn và khiến thời gian chạy tổng thể chậm hơn.

FYI - Trình phân vùng được sử dụng có thể được kiểm soát bằng cách sử dụng quá tải thích hợp cho Parallel.ForEach , nếu muốn. Để biết chi tiết, xem Phân vùng tùy chỉnh trên MSDN.

Sự khác biệt chính, tại thời gian chạy, là thứ hai sẽ hoạt động không đồng bộ. Điều này có thể được nhân đôi bằng Parallel.ForEach bằng cách thực hiện:

Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));

Bằng cách này, bạn vẫn tận dụng các trình phân vùng, nhưng không chặn cho đến khi hoàn thành thao tác.


8
IIRC, phân vùng mặc định được thực hiện bởi Parallel.ForEach cũng tính đến số lượng luồng phần cứng có sẵn, giúp bạn không phải làm việc với số lượng Nhiệm vụ tối ưu để bắt đầu. Kiểm tra bài viết Các mẫu lập trình song song của Microsoft ; nó có những lời giải thích tuyệt vời về tất cả những thứ này trong đó.
Mal Ross

2
@Mal: Sắp xếp ... Đó thực sự không phải là Trình phân vùng, mà là công việc của TaskScheduler. Theo mặc định, TaskScheduler sử dụng ThreadPool mới, xử lý việc này rất tốt.
Sậy Copsey

Cảm ơn. Tôi biết rằng tôi nên rời đi trong cảnh báo "Tôi không phải là chuyên gia, nhưng ...". :)
Mal Ross

@ReedCopsey: Làm cách nào để đính kèm các tác vụ bắt đầu qua Parallel.ForEach vào tác vụ trình bao bọc? Vì vậy, khi bạn gọi .Wait () trên tác vụ trình bao bọc, nó sẽ bị treo cho đến khi các tác vụ chạy song song được hoàn thành?
Konstantin Tarkus

1
@Tarkus Nếu bạn đang thực hiện nhiều yêu cầu, tốt hơn hết là bạn chỉ nên sử dụng HttpClient.GetString trong mỗi mục công việc (trong vòng lặp Song song của bạn). Không có lý do để đặt tùy chọn không đồng bộ bên trong vòng lặp đã đồng thời, thường là ...
Reed Copsey

89

Tôi đã thực hiện một thử nghiệm nhỏ khi chạy phương thức "1.000.000.000 (một tỷ)" lần với "Parallel.For" và một với các đối tượng "Nhiệm vụ".

Tôi đo thời gian xử lý và thấy Parallel hiệu quả hơn. Parallel.For phân chia nhiệm vụ của bạn thành các mục công việc nhỏ và thực hiện chúng trên tất cả các lõi theo cách tối ưu. Trong khi tạo nhiều đối tượng tác vụ (FYI TPL sẽ sử dụng tổng hợp luồng trong nội bộ) sẽ di chuyển mọi thực thi trên mỗi tác vụ tạo thêm căng thẳng trong hộp hiển nhiên từ thử nghiệm bên dưới.

Tôi cũng đã tạo một video nhỏ giải thích TPL cơ bản và cũng trình bày cách Parallel.For sử dụng lõi của bạn hiệu quả hơn http://www.youtube.com/watch?v=No7QqSc5cl8 so với các tác vụ và chủ đề thông thường.

Thí nghiệm 1

Parallel.For(0, 1000000000, x => Method1());

Thí nghiệm 2

for (int i = 0; i < 1000000000; i++)
{
    Task o = new Task(Method1);
    o.Start();
}

So sánh thời gian xử lý


Nó sẽ hiệu quả hơn và lý do đằng sau việc tạo ra các chủ đề tốn kém Thí nghiệm 2 là một thực tế rất tồi.
Tim

@ Georgi-nó vui lòng quan tâm đến việc nói nhiều hơn về những gì xấu.
Shivprasad Koirala

3
Tôi xin lỗi, sai lầm của tôi, tôi nên đã làm rõ. Ý tôi là việc tạo các Nhiệm vụ trong một vòng lặp đến 1000000000. Chi phí không thể tưởng tượng được. Chưa kể rằng Parallel không thể tạo ra hơn 63 nhiệm vụ cùng một lúc, điều này làm cho nó được tối ưu hóa hơn nhiều trong trường hợp.
Georgi-it

Điều này đúng với 1000000000 nhiệm vụ. Tuy nhiên, khi tôi xử lý một hình ảnh (lặp đi lặp lại, phóng to fractal) và thực hiện Parallel.For trên các dòng, rất nhiều lõi không hoạt động trong khi chờ các luồng cuối cùng kết thúc. Để làm cho nó nhanh hơn, tôi tự chia dữ liệu thành 64 gói công việc và tạo các tác vụ cho nó. (Sau đó, Nhiệm vụ
Tedd Hansen

1
Làm gì Mehthod1()trong ví dụ này?
Zapnologica

17

Parallel.ForEach sẽ tối ưu hóa (thậm chí có thể không bắt đầu các luồng mới) và chặn cho đến khi vòng lặp kết thúc và Task.Factory sẽ rõ ràng tạo một thể hiện nhiệm vụ mới cho mỗi mục và trả về trước khi chúng kết thúc (các tác vụ không đồng bộ). Parallel.Foreach hiệu quả hơn nhiều.


11

Theo quan điểm của tôi, kịch bản thực tế nhất là khi các tác vụ phải hoàn thành một hoạt động nặng. Cách tiếp cận của Shivprasad tập trung nhiều vào việc tạo đối tượng / phân bổ bộ nhớ hơn là vào chính máy tính. Tôi đã thực hiện một nghiên cứu gọi phương pháp sau:

public static double SumRootN(int root)
{
    double result = 0;
    for (int i = 1; i < 10000000; i++)
        {
            result += Math.Exp(Math.Log(i) / root);
        }
        return result; 
}

Thực hiện phương pháp này mất khoảng 0,5 giây.

Tôi đã gọi nó 200 lần bằng cách sử dụng Parallel:

Parallel.For(0, 200, (int i) =>
{
    SumRootN(10);
});

Sau đó, tôi gọi nó 200 lần bằng cách sử dụng kiểu cũ:

List<Task> tasks = new List<Task>() ;
for (int i = 0; i < loopCounter; i++)
{
    Task t = new Task(() => SumRootN(10));
    t.Start();
    tasks.Add(t);
}

Task.WaitAll(tasks.ToArray()); 

Trường hợp đầu tiên hoàn thành trong 26656ms, lần thứ hai trong 24478ms. Tôi đã lặp lại nó nhiều lần. Mỗi lần tiếp cận thứ hai là nhanh hơn.


Sử dụng Parallel.For là cách thức cũ. Sử dụng Nhiệm vụ được khuyến nghị cho các đơn vị công việc không thống nhất. Các MVP và nhà thiết kế của TPL của Microsoft cũng đề cập rằng việc sử dụng Nhiệm vụ sẽ sử dụng các luồng hiệu quả hơn, chặn ienot càng nhiều trong khi chờ các đơn vị khác hoàn thành.
Suncat2000
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.