看看Parallel中高度封裝的三個方法,Invoke,For和ForEach

一線碼農發表於2014-11-01

  說到.net中的並行程式設計,也許你的第一反應就是Task,確實Task是一個非常靈活的用於並行程式設計的一個專用類,不可否認越靈活的東西用起來就越

複雜,高度封裝的東西用起來很簡單,但是缺失了靈活性,這篇我們就看看這些好用但靈活性不高的幾個並行方法。

 

一:Invoke

  現在電子商務的網站都少不了訂單的流程,沒有訂單的話網站也就沒有存活的價值了,往往在訂單提交成功後,通常會有這兩個操作,第一個:發起

信用卡扣款,第二個:傳送emial確認單,這兩個操作我們就可以在下單介面呼叫成功後,因為兩個方法是互不干擾的,所以就可以用invoke來玩玩了。

 1         static void Main(string[] args)
 2         {
 3             Parallel.Invoke(Credit, Email);
 4 
 5             Console.Read();
 6         }
 7 
 8         static void Credit()
 9         {
10             Console.WriteLine("******************  發起信用卡扣款中  ******************");
11 
12             Thread.Sleep(2000);
13 
14             Console.WriteLine("扣款成功!");
15         }
16 
17         static void Email()
18         {
19             Console.WriteLine("******************  傳送郵件確認單!*****************");
20 
21             Thread.Sleep(3000);
22 
23             Console.WriteLine("email傳送成功!");
24         }

 

 

  怎麼樣,實現起來是不是很簡單,只要把你需要的方法塞給invoke就行了,不過在這個方法裡面有一個過載引數需要注意下,

1  public static void Invoke(ParallelOptions parallelOptions, params Action[] actions);

 

有時候我們的執行緒可能會跑遍所有的核心,為了提高其他應用程式的穩定性,就要限制參與的核心,正好ParallelOptions提供了

MaxDegreeOfParallelism屬性。

 

好了,下面我們大概翻翻invoke裡面的程式碼實現,發現有幾個好玩的地方:

 

<1>: 當invoke中的方法超過10個話,我們發現它走了一個internal可見的ParallelForReplicatingTask的FCL內部專用類,而這個類是繼承自

   Task的,當方法少於10個的話,才會走常規的Task.

<2> 居然發現了一個裝exception 的ConcurrentQueue<Exception>佇列集合,多個異常入隊後,再包裝成AggregateException丟擲來。

       比如:throw new AggregateException(exceptionQ);

<3> 我們發現,不管是超過10個還是小於10個,都是通過WaitAll來等待所有的執行,所以缺點就在這個地方,如果某一個方法執行時間太長

   不能退出,那麼這個方法是不是會長期掛在這裡不能出來,也就導致了主流程一直掛起,然後頁面就一直掛起,所以這個是一個非常危險

      的行為,如果我們用task中就可以在waitall中設定一個過期時間,但invoke卻沒法做到,所以在使用invoke的時候要慎重考慮。

  1     try
  2     {
  3         if (actionsCopy.Length > 10 || (parallelOptions.MaxDegreeOfParallelism != -1 && parallelOptions.MaxDegreeOfParallelism < actionsCopy.Length))
  4         {
  5             ConcurrentQueue<Exception> exceptionQ = null;
  6             try
  7             {
  8                 int actionIndex = 0;
  9                 ParallelForReplicatingTask parallelForReplicatingTask = new ParallelForReplicatingTask(parallelOptions, delegate
 10                 {
 11                     for (int l = Interlocked.Increment(ref actionIndex); l <= actionsCopy.Length; l = Interlocked.Increment(ref actionIndex))
 12                     {
 13                         try
 14                         {
 15                             actionsCopy[l - 1]();
 16                         }
 17                         catch (Exception item)
 18                         {
 19                             LazyInitializer.EnsureInitialized<ConcurrentQueue<Exception>>(ref exceptionQ, () => new ConcurrentQueue<Exception>());
 20                             exceptionQ.Enqueue(item);
 21                         }
 22                         if (parallelOptions.CancellationToken.IsCancellationRequested)
 23                         {
 24                             throw new OperationCanceledException(parallelOptions.CancellationToken);
 25                         }
 26                     }
 27                 }, TaskCreationOptions.None, InternalTaskOptions.SelfReplicating);
 28                 parallelForReplicatingTask.RunSynchronously(parallelOptions.EffectiveTaskScheduler);
 29                 parallelForReplicatingTask.Wait();
 30             }
 31             catch (Exception ex2)
 32             {
 33                 LazyInitializer.EnsureInitialized<ConcurrentQueue<Exception>>(ref exceptionQ, () => new ConcurrentQueue<Exception>());
 34                 AggregateException ex = ex2 as AggregateException;
 35                 if (ex != null)
 36                 {
 37                     using (IEnumerator<Exception> enumerator = ex.InnerExceptions.GetEnumerator())
 38                     {
 39                         while (enumerator.MoveNext())
 40                         {
 41                             Exception current = enumerator.Current;
 42                             exceptionQ.Enqueue(current);
 43                         }
 44                         goto IL_264;
 45                     }
 46                 }
 47                 exceptionQ.Enqueue(ex2);
 48                 IL_264:;
 49             }
 50             if (exceptionQ != null && exceptionQ.Count > 0)
 51             {
 52                 Parallel.ThrowIfReducableToSingleOCE(exceptionQ, parallelOptions.CancellationToken);
 53                 throw new AggregateException(exceptionQ);
 54             }
 55         }
 56         else
 57         {
 58             Task[] array = new Task[actionsCopy.Length];
 59             if (parallelOptions.CancellationToken.IsCancellationRequested)
 60             {
 61                 throw new OperationCanceledException(parallelOptions.CancellationToken);
 62             }
 63             for (int j = 0; j < array.Length; j++)
 64             {
 65                 array[j] = Task.Factory.StartNew(actionsCopy[j], parallelOptions.CancellationToken, TaskCreationOptions.None, InternalTaskOptions.None, parallelOptions.EffectiveTaskScheduler);
 66             }
 67             try
 68             {
 69                 if (array.Length <= 4)
 70                 {
 71                     Task.FastWaitAll(array);
 72                 }
 73                 else
 74                 {
 75                     Task.WaitAll(array);
 76                 }
 77             }
 78             catch (AggregateException ex3)
 79             {
 80                 Parallel.ThrowIfReducableToSingleOCE(ex3.InnerExceptions, parallelOptions.CancellationToken);
 81                 throw;
 82             }
 83             finally
 84             {
 85                 for (int k = 0; k < array.Length; k++)
 86                 {
 87                     if (array[k].IsCompleted)
 88                     {
 89                         array[k].Dispose();
 90                     }
 91                 }
 92             }
 93         }
 94     }
 95     finally
 96     {
 97         if (TplEtwProvider.Log.IsEnabled())
 98         {
 99             TplEtwProvider.Log.ParallelInvokeEnd((task != null) ? task.m_taskScheduler.Id : TaskScheduler.Current.Id, (task != null) ? task.Id : 0, forkJoinContextID);
100         }
101     }

 

二:For

   下面再看看Parallel.For,我們知道普通的For是一個序列操作,如果說你的for中每條流程都需要執行一個方法,並且這些方法可以並行操作且

比較耗時,那麼為何不嘗試用Parallel.For呢,就比如下面的程式碼。

 1 class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             List<Action> actions = new List<Action>() { Credit, Email };
 6 
 7             var result = Parallel.For(0, actions.Count, (i) =>
 8             {
 9                 actions[i]();
10             });
11 
12             Console.WriteLine("執行狀態:" + result.IsCompleted);
13 
14             Console.Read();
15         }
16 
17         static void Credit()
18         {
19             Console.WriteLine("******************  發起信用卡扣款中  ******************");
20 
21             Thread.Sleep(2000);
22 
23             Console.WriteLine("扣款成功!");
24         }
25 
26         static void Email()
27         {
28             Console.WriteLine("******************  傳送郵件確認單!*****************");
29 
30             Thread.Sleep(3000);
31 
32             Console.WriteLine("email傳送成功!");
33         }
34     }


下面我們再看看Parallel.For中的最簡單的過載和最複雜的過載:

1 public static ParallelLoopResult For(int fromInclusive, int toExclusive, Action<int> body);
2 
3 public static ParallelLoopResult For<TLocal>(int fromInclusive, int toExclusive, ParallelOptions parallelOptions, Func<TLocal> localInit, Func<int, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally);
4  

 

<1> 簡單的過載不必多說,很簡單,我上面的例子也演示了。

<2> 最複雜的這種過載提供了一個AOP的功能,在每一個body的action執行之前會先執行localInit這個action,在body之後還會執行localFinally

       這個action,有沒有感覺到已經把body切成了三塊?好了,下面看一個例子。

 

 1     static void Main(string[] args)
 2         {
 3             var list = new List<int>() { 10, 20, 30, 40 };
 4 
 5             var options = new ParallelOptions();
 6 
 7             var total = 0;
 8 
 9             var result = Parallel.For(0, list.Count, () =>
10             {
11                 Console.WriteLine("------------  thead --------------");
12 
13                 return 1;
14             },
15               (i, loop, j) =>
16               {
17                   Console.WriteLine("------------  body --------------");
18 
19                   Console.WriteLine("i=" + list[i] + " j=" + j);
20 
21                   return list[i];
22               },
23               (i) =>
24               {
25                   Console.WriteLine("------------  tfoot --------------");
26 
27                   Interlocked.Add(ref total, i);
28 
29                   Console.WriteLine("total=" + total);
30               });
31 
32             Console.WriteLine("iscompleted:" + result.IsCompleted);
33             Console.Read();
34         }
View Code

 

接下來我們再翻翻它的原始碼,由於原始碼太多,裡面神乎其神,我就找幾個好玩的地方。

<1>  我在裡面找到了一個rangeManager分割槽函式,程式碼複雜看不懂,貌似很強大。

 1         internal RangeManager(long nFromInclusive, long nToExclusive, long nStep, int nNumExpectedWorkers)
 2         {
 3             this.m_nCurrentIndexRangeToAssign = 0;
 4             this.m_nStep = nStep;
 5             if (nNumExpectedWorkers == 1)
 6             {
 7                 nNumExpectedWorkers = 2;
 8             }
 9             ulong num = (ulong)(nToExclusive - nFromInclusive);
10             ulong num2 = num / (ulong)((long)nNumExpectedWorkers);
11             num2 -= num2 % (ulong)nStep;
12             if (num2 == 0uL)
13             {
14                 num2 = (ulong)nStep;
15             }
16             int num3 = (int)(num / num2);
17             if (num % num2 != 0uL)
18             {
19                 num3++;
20             }
21             long num4 = (long)num2;
22             this.m_indexRanges = new IndexRange[num3];
23             long num5 = nFromInclusive;
24             for (int i = 0; i < num3; i++)
25             {
26                 this.m_indexRanges[i].m_nFromInclusive = num5;
27                 this.m_indexRanges[i].m_nSharedCurrentIndexOffset = null;
28                 this.m_indexRanges[i].m_bRangeFinished = 0;
29                 num5 += num4;
30                 if (num5 < num5 - num4 || num5 > nToExclusive)
31                 {
32                     num5 = nToExclusive;
33                 }
34                 this.m_indexRanges[i].m_nToExclusive = num5;
35             }
36         }

 

<2> 我又找到了這個神奇的ParallelForReplicatingTask類。

 

那麼下面問題來了,在單執行緒的for中,我可以continue,可以break,那麼在Parallel.For中有嗎?因為是並行,所以continue基本上就沒有

存在價值,break的話確實有價值,這個就是委託中的ParallelLoopState做到的,並且還新增了一個Stop。

 

 

三:ForEach

其實ForEach和for在本質上是一樣的,你在原始碼中會發現在底層都是呼叫一個方法的,而ForEach會在底層中呼叫for共同的函式之前還會執行

其他的一些邏輯,所以這就告訴我們,能用Parallel.For的地方就不要用Parallel.ForEach,其他的都一樣了,這裡就不贅述了。

 

相關文章