2013-01-02 14 views
28

Trong ứng dụng của tôi, tôi thực thi từ vài chục đến vài trăm hành động song song (không trả về giá trị cho các hành động).Task.Factory.StartNew vs. Parallel.Invoke

Những cách tiếp cận sẽ là tối ưu nhất:

  1. Sử dụng Task.Factory.StartNew trong foreach vòng lặp iterating qua Action mảng (Action[])

    Task.Factory.StartNew(() => someAction());

  2. Sử dụng Parallel lớp nơi actionsAction mảng (Action[])

    Parallel.Invoke(actions);

Là những hai cách tiếp cận tương đương? Có bất kỳ tác động hiệu suất nào không?

EDIT

Tôi đã thực hiện một số xét nghiệm hiệu suất và trên máy tính của tôi (2 CPU 2 lõi mỗi) kết quả có vẻ là rất giống nhau. Tôi không chắc nó trông như thế nào trên các máy khác như 1 CPU. Ngoài ra tôi không chắc chắn (không biết làm thế nào để kiểm tra nó rất chính xác cách) tiêu thụ bộ nhớ là gì.

+3

Tôi nghĩ rằng chúng ít nhiều tương đương. Hồ sơ của bạn cho bạn biết điều gì? –

+0

bạn đã xem xét điểm chuẩn chưa? –

+1

Kiểm tra bài đăng nhỏ đẹp này về 2 phương pháp: http://blackrabbitcoder.net/archive/2012/12/20/c.net-little-wonders-the-parallel.invoke-method.aspx. Tôi không nghĩ rằng có bất kỳ sự khác biệt hiệu suất mặc dù, tôi nghĩ rằng Parallel.Invoke chỉ là dễ dàng hơn. –

Trả lời

41

Sự khác biệt quan trọng nhất giữa hai là Parallel.Invoke sẽ đợi cho tất cả các hành động để hoàn thành trước khi tiếp tục với mã, trong khi StartNew sẽ chuyển sang dòng tiếp theo của mã, cho phép các nhiệm vụ để hoàn thành trong của mình thời gian tốt.

Sự khác biệt ngữ nghĩa này phải là sự cân nhắc đầu tiên (và có thể là duy nhất) của bạn. Nhưng đối với mục đích thông tin, đây là một chuẩn mực:

/* This is a benchmarking template I use in LINQPad when I want to do a 
* quick performance test. Just give it a couple of actions to test and 
* it will give you a pretty good idea of how long they take compared 
* to one another. It's not perfect: You can expect a 3% error margin 
* under ideal circumstances. But if you're not going to improve 
* performance by more than 3%, you probably don't care anyway.*/ 
void Main() 
{ 
    // Enter setup code here 
    var actions2 = 
    (from i in Enumerable.Range(1, 10000) 
    select (Action)(() => {})).ToArray(); 

    var awaitList = new Task[actions2.Length]; 
    var actions = new[] 
    { 
     new TimedAction("Task.Factory.StartNew",() => 
     { 
      // Enter code to test here 
      int j = 0; 
      foreach(var action in actions2) 
      { 
       awaitList[j++] = Task.Factory.StartNew(action); 
      } 
      Task.WaitAll(awaitList); 
     }), 
     new TimedAction("Parallel.Invoke",() => 
     { 
      // Enter code to test here 
      Parallel.Invoke(actions2); 
     }), 
    }; 
    const int TimesToRun = 100; // Tweak this as necessary 
    TimeActions(TimesToRun, actions); 
} 


#region timer helper methods 
// Define other methods and classes here 
public void TimeActions(int iterations, params TimedAction[] actions) 
{ 
    Stopwatch s = new Stopwatch(); 
    int length = actions.Length; 
    var results = new ActionResult[actions.Length]; 
    // Perform the actions in their initial order. 
    for(int i = 0; i < length; i++) 
    { 
     var action = actions[i]; 
     var result = results[i] = new ActionResult{Message = action.Message}; 
     // Do a dry run to get things ramped up/cached 
     result.DryRun1 = s.Time(action.Action, 10); 
     result.FullRun1 = s.Time(action.Action, iterations); 
    } 
    // Perform the actions in reverse order. 
    for(int i = length - 1; i >= 0; i--) 
    { 
     var action = actions[i]; 
     var result = results[i]; 
     // Do a dry run to get things ramped up/cached 
     result.DryRun2 = s.Time(action.Action, 10); 
     result.FullRun2 = s.Time(action.Action, iterations); 
    } 
    results.Dump(); 
} 

public class ActionResult 
{ 
    public string Message {get;set;} 
    public double DryRun1 {get;set;} 
    public double DryRun2 {get;set;} 
    public double FullRun1 {get;set;} 
    public double FullRun2 {get;set;} 
} 

public class TimedAction 
{ 
    public TimedAction(string message, Action action) 
    { 
     Message = message; 
     Action = action; 
    } 
    public string Message {get;private set;} 
    public Action Action {get;private set;} 
} 

public static class StopwatchExtensions 
{ 
    public static double Time(this Stopwatch sw, Action action, int iterations) 
    { 
     sw.Restart(); 
     for (int i = 0; i < iterations; i++) 
     { 
      action(); 
     } 
     sw.Stop(); 

     return sw.Elapsed.TotalMilliseconds; 
    } 
} 
#endregion 

Kết quả:

Message    | DryRun1 | DryRun2 | FullRun1 | FullRun2 
---------------------------------------------------------------- 
Task.Factory.StartNew | 43.0592 | 50.847 | 452.2637 | 463.2310 
Parallel.Invoke  | 10.5717 | 9.948 | 102.7767 | 101.1158 

Như bạn thấy, sử dụng Parallel.Invoke có thể xấp xỉ 4.5x nhanh hơn so với chờ đợi một loạt các nhiệm vụ newed-up hoàn thành. Tất nhiên, đó là khi hành động của bạn hoàn toàn không có gì. Mỗi hành động càng có nhiều thì bạn càng nhận thấy sự khác biệt.

+1

cảm ơn bạn đã thêm thời gian – BigChief

+0

Xem phần này: http://stackoverflow.com/questions/16101811/task-waitall-method-vs-parallel-invoke-method – nawfal

12

Trong sơ đồ lớn của sự vật, sự khác biệt về hiệu suất giữa hai phương pháp là không đáng kể khi xem xét chi phí thực sự xử lý nhiều nhiệm vụ trong mọi trường hợp.

Parallel.Invoke về cơ bản sẽ thực hiện Task.Factory.StartNew() cho bạn. Vì vậy, tôi muốn nói khả năng đọc là quan trọng hơn ở đây.

Ngoài ra, như StriplingWarrior đề cập, các Parallel.Invoke thực hiện một WaitAll (chặn mã cho đến khi tất cả các nhiệm vụ được hoàn thành) cho bạn, vì vậy bạn không phải làm điều đó. Nếu bạn muốn các tác vụ chạy trong nền mà không cần quan tâm khi hoàn thành, thì bạn muốn Task.Factory.StartNew().

12

Tôi đã sử dụng các kiểm tra từ StriplingWarror để tìm ra sự khác biệt xuất phát từ đâu.Tôi đã làm điều này bởi vì khi tôi nhìn với Reflector tại mã lớp Parallel không có gì khác hơn là tạo ra một loạt các nhiệm vụ và cho phép chúng chạy.

Từ quan điểm lý thuyết, cả hai phương pháp tiếp cận phải tương đương về thời gian chạy. Nhưng như các bài kiểm tra (không thực tế) với một hành động rỗng đã cho thấy rằng lớp Parallel nhanh hơn nhiều.

Phiên bản tác vụ dành hầu hết thời gian của mình bằng cách tạo các tác vụ mới dẫn đến nhiều bộ sưu tập rác. Sự khác biệt về tốc độ bạn thấy hoàn toàn là do bạn tạo ra nhiều nhiệm vụ nhanh chóng trở thành rác.

Lớp Parallel thay vì tạo ra lớp dẫn xuất nhiệm vụ của riêng nó mà chạy đồng thời trên tất cả các CPU. Chỉ có một nhiệm vụ phyiscal chạy ở tất cả các lõi. Việc đồng bộ hóa xảy ra bên trong nhiệm vụ ủy nhiệm bây giờ, nó giải thích tốc độ nhanh hơn nhiều của lớp Parallel.

ParallelForReplicatingTask task2 = new ParallelForReplicatingTask(parallelOptions, delegate { 
     for (int k = Interlocked.Increment(ref actionIndex); k <= actionsCopy.Length; k = Interlocked.Increment(ref actionIndex)) 
     { 
      actionsCopy[k - 1](); 
     } 
    }, TaskCreationOptions.None, InternalTaskOptions.SelfReplicating); 
task2.RunSynchronously(parallelOptions.EffectiveTaskScheduler); 
task2.Wait(); 

Vậy điều gì tốt hơn? Nhiệm vụ tốt nhất là nhiệm vụ không bao giờ chạy. Nếu bạn cần tạo nhiều nhiệm vụ như vậy, chúng sẽ trở thành gánh nặng cho bộ thu gom rác, bạn nên tránh xa các API nhiệm vụ và dính vào lớp Parallel cho phép bạn thực hiện song song trực tiếp tại tất cả các lõi mà không có nhiệm vụ mới.

Nếu bạn cần nhanh hơn, có thể tạo chủ đề bằng tay và sử dụng cấu trúc dữ liệu được tối ưu hóa bằng tay để cung cấp cho bạn tốc độ tối đa cho mẫu truy cập của bạn là giải pháp hiệu suất nhất. Nhưng không chắc rằng bạn sẽ thành công khi làm như vậy vì các API TPL và Parallel đã được điều chỉnh rất nhiều. Thông thường, bạn cần phải sử dụng một trong nhiều quá tải để cấu hình các tác vụ đang chạy của bạn hoặc lớp Parallel để đạt được điều tương tự với mã ít hơn nhiều.

Nhưng nếu bạn có mô hình luồng không chuẩn, có thể bạn nên tắt mà không cần sử dụng TPL để tận dụng tối đa lõi của mình. Ngay cả Stephen Toub đã đề cập rằng các API TPL không được thiết kế cho hiệu suất cực nhanh nhưng mục tiêu chính là làm cho luồng dễ dàng hơn cho lập trình viên "trung bình". Để đánh bại TPL trong những trường hợp cụ thể, bạn cần phải ở trên mức trung bình và bạn cần phải biết nhiều thứ về các dòng bộ nhớ cache CPU, lập lịch trình luồng, mô hình bộ nhớ, tạo mã JIT, ... để xuất hiện trong kịch bản cụ thể của bạn tốt hơn.