顯示具有 多執行緒 標籤的文章。 顯示所有文章
顯示具有 多執行緒 標籤的文章。 顯示所有文章

2013年9月21日 星期六

.Net中各種Lock物件的比較

表1. 各種鎖在Intel Core i7 860的成本比較
建構方式 .Net Framework版本 目的 是否可跨Process 成本
lock(Monitor.Enter/Monitor/Exit) 2.0 確保同一時間內僅有一條執行緒可以存取資源。 20ns
Mutex 2.0 確保同一時間內僅有一條執行緒可以存取資源。 1000ns
SemaphoreSlim 4.0 確保同一時間內僅有指定的執行緒個數可以存取資源。 200ns
Semaphore 2.0 確保同一時間內僅有指定的執行緒個數可以存取資源。 1000ns
ReaderWriterLockSlim 3.5 允許多個讀取型執行緒,但僅有一個寫入型執行緒可以存取資源。 40ns
ReaderWriterLock(棄用) 2.0 允許多個讀取型執行緒,但僅有一個寫入型執行緒可以存取資源。 100ns
 
Lock   
 
Lock是一種物件鎖,因此,它所鎖定的只能是一個參考型變數,不能是值類型的變數。當有一個執行緒取得該物件鎖則在它之後的其它執行緒只能等待第一個執行緒釋放物件鎖。在使用上要注意不同的執行緒是否是將同一個物件來看待成鎖;譬如說,在一段Script Block中宣告了一個物件並且針對該物件作為鎖,這個情境是沒有同步資源存取的能力,因為每個執行執行到這段Script Block後就會自行建立物件並且取得物件鎖。    在實務上,通常會挑選類別中private static Object lockObj作為物件鎖的最佳對像;因為它可以確保它是全域中獨一無二的物件,在使用上比較能夠避開誤用物件當成鎖的對像的狀況。   
 
有三種避免作為物件鎖的物件:   
   1. lock(this)   
   2. lock(typeof(SomeType))   
   3. lock("SomeString")   
 
   這三種都有可能發生與預期的鎖定效果有出入的狀況。大多數的集合物件都會提供一個作為物件鎖的內部變數: _syncRoot。但是此一成員在.Net Framework 3.0之後已不再提供給開發人員使用了。
class SomeClass
{
   private static object lockObj = new object();
   public void DoSomething()
   {
      lock (lockObj)
      {
         //工作
       }
    }
}

Mutex
   其作用為一個同步鎖,用來進行兩個工作對同一個資源存取的控制,藉以避免誤動作或是結果錯誤。Mutex和Monitor很類似只有擁有Mutex物件的執行緒才具有存取資源的權限,由於Mutex物件僅能有一個,也因此確立了資源不會同時被多個執行緒所存取;只有在擁有資源存取權的執行緒發出釋放Mutex訊號之後,其它執行緒才能存取資源。
 
   Mutex較Monitor複雜的地方是在於Mutex允許跨執行元進行資源的鎖定。Mutex在內部是呼叫作業系統的API,也因此它可以跨執行元更也因此它消耗的資源更多。Mutex在鎖定時像是Lock,亦即Monitor.Enter()方法而不是Monitor.Wait()。
 
   Mutex的方法:  
   1. WaitOne(): 請求資源存取權,會一直持有鎖定到Mutex收到釋放訊號為止。
   2. ReleaseMutex(): 釋放Mutex。Mutex的內部計數器由CLR維護,每呼叫WaitOne()一次則Mutext計數器+1,當呼叫ReleaseMutex()便將計數器-1。只要計數不為0,等待資源的執行緒就會繼續等待。
 
   Mutex最糟的使用情境就是擁有Mutex物件鎖的執行緒執行工作到一半便自行結束,且結束前沒有對Mutex物件發出釋放訊號,這種狀況是最惡劣的使用Mutex物件情境,故使用Mutex時必定要是Try/Finally中使用,並且在Finally中對Mutex物件發出釋放訊號。
 
   Mutex的建構子有五個重載,而每一個建構子有著不同的操作目的:
   1. Mutex(): 無法進行跨執行元進行資源鎖定,亦為局部(Local)型同步鎖。
 
   2. Mutex(bool): 與1相同,僅能作為局域型同步鎖,而輸入參數是指示實體化Mutex的執行緒是否在實體化後馬上取得同步鎖。
 
   3. Mutex(bool, string): 與2.不同之處在於3.可以跨執行元進行資源鎖定,亦即全域型同步鎖,但是不建議採用這一種方式,因為無法得知是否這個名稱的Mutex物件已在其它執行元建立過並且被取走同步鎖。名稱具大小寫敏感。
 
   4. Mutex(bool, string, out bool): 第三個輸入參數用來指示是否取得同步鎖,是實務中較常用到的。
 
   5. Mutex(bool, string, out bool, MutexSecurity): 最後一個參數是用來進行帳戶安全性控制;控制僅有某幾個帳戶能夠存取某個名稱的Mutex物件的同步鎖。
bool blLock;
Mutex mutex = new Mutex( true, @"Global\MyAPP", out blLock);

try {
   if(blLock)
      //工作
   else
      //等待同步鎖
}
finally {
    mutex.ReleaseMutex();
}

Semaphore/SemaphoreSlim
 
   Semaphore與Mutex類似,但Semaphore可以允許資源給予多個執行緒存取;Semaphore類似一個提供計數的Mutex,可定義執行緒存取個數。同樣地,盡量在Try/Finally中使用Semaphore。
 
   當某些資源是限定僅有幾個執行緒存取時,可以考慮使用Semaphore;譬如,現在僅有3個Port可以使用,而這個時候就以設定Semaphore計數為3,而第四位執行緒就需等待。
 
   在.Net Framework 4.0中提供了Semaphore的輕量級版本-SemaphoreSlim。SemaphoreSlim不具有Semaphore可以跨執行元的能力,故所需資源較Semaphore少。Semaphore同Mutex都是在建構子中指定名稱以作為全域型同步鎖,而名稱同Mutex是大小寫敏感。
 
   Semaphore從機制上來說較類似於Mutex一樣是一種鎖,而不是"通知"。因為Semaphore允許多個執行緒存取同一個資源,因此,Semaphore本身是執行緒非關的,亦即非Semaphore擁有者可以發出釋放訊號,並且可以設定釋放幾個鎖: Release(N),但是當指定釋放的鎖的個數超過Semaphore中所設定的會拋出SemaphoreFullException例外。
Semaphore semaphore = new Semaphore(1, 3);
semaphore.WaitOne();

//執行工作
semaphore.Release();

ReaderWriterLockSlim/ReaderWriterLock
 
   和Monitor相同是.Net所撰寫的原生類別,因此在底層並不會去呼叫到OS的API。ReaderWriterLock在運作上是將讀取資源與寫入資源拆分開來看;當執行緒的工作可以被明顯區分成讀取與寫入兩類並且寫入的工作既短且快時ReaderWriterLock是相當好的同步鎖。
 
   ReaderWriterLock本質上仍是同一時間僅能只有"一組"執行緒存取資源;當寫入組取得同步鎖時,讀取組僅能等待寫入組釋放同步鎖,但是較特別的是寫入組一次僅能有一個執行緒取得同步鎖,但是讀取組可以有多個執行緒同時讀取資源。
 
   ReaderWriterLock在處理不當時非常容易造成"飢餓現象";這是因為若寫入組占用太長時間或是兩組中有某個執行緒忘了釋放資源,這是就會造成另一組執行緒永遠無法存取資源,通常會採用時間限制,若在時限內沒有取得同步鎖時便拋出ApplicationException異常。
 
   ReaderWriterLock不允許一個執行緒同時擔任寫入與讀取組,ReaderWriterLock常用方法:
   1. AcquireReaderLock(): 請求讀取同步鎖。
   2. AcquireWriterLock(): 請求寫入同步鎖。
   3. ReleaseReaderLock(): 讀取鎖-1;當讀取鎖為0時,便換寫入組取得資源存取權。
   4. ReleaseWriterLock(): 釋放寫入鎖。
   5. ReleaseLock(): 釋放鎖;不論當前是處於計數寫入或是讀取。
 
EventWaitHandler一族
 
   EventWaitHandler在應用上都是以"通知"為主軸;在某些情境下希望執行緒在工作完成之後能夠通知已完成,讓整個系統的運行更順暢而不需要以輪詢(Pooling)這種低效能的作法判斷是否執行緒已經完成工作。
 
   EventWaitHandler僅有通知能力本身並不具有資源鎖定的能力,因此,在實務中通常會搭配Lock/Monitor/Mutex一起實做平行系統;共有兩組執行緒,一組擔任生產者,另一組則是擔任消費者並且由Lock/Monitor/Mutex進行資源的鎖定,開始時僅有生產者開始工作另一組消費者則是被阻擋,當生產者完工之後會通知消費者進行消費並且自身進入阻擋狀態,待消費者組完成工作後再通知生產者工作,如此往復循環。
 
   EventWaitHandle有以下幾個常用的方法:
 
   1. SingnalAndWait(): EventWaitHandle的靜態方法;通知另一個EventWaitHandle並且自身進入等待狀態
   2. WaitAny(): EventWaitHandle的靜態方法;等待一組EventWaitHandle中某個EventWaitHandle進入執行狀態
   3. WaitAll(): EventWaitHandle的靜態方法;等待一組執行緒內所有EventWaitHandle進入執行狀態
   4. Set(): 進入執行狀態
   5. Reset(): 進入等待狀態
   EventWaitHandle的建構子不同重載具不同能力:
   1. EventWaitHandle(bool, EventResetMode, String): 第三個參數用在全域型控制時使用,而第二個參數有: AutoReset/ManualReset兩個列舉值;AutoReset是當呼叫Set()之後釋放處於等待的一個執行緒後自動進入等待狀態,其餘執行緒依舊處於等待,ManualReset會釋放所有等待中的執行緒,並在手動呼叫Reset()前,一直處於通行狀態。
 
   2. EventWaitHandle(bool ,EventResetMode ,string ,out bool ,EventWaitHandleSecurity): 後面兩參數同Mutex。
   EventWaitHandle的子類別: AutoResetEvent/ManualResetEvent,其特性就如同EventResetMod一般。
 
   AutoResetEvent(自動重置事件)
 
   在AutoResetEvent的建構子: public AutoResetEvent(bool InitialState),在建構子中使用bool型別的輸入參數來作為初始狀態的設定;若要將AutoResetEvent的初始狀態設定為:
   1. 終止: 輸入值為true
   2. 非終止: 輸入值為false
 
   一般來說,AutoResetEvent常用到的方法有兩個:
 
   1. WaitOne(int millisecondsTimeout): 這個方法是用來讓呼叫它的執行緒進入等待狀態,並且依傳入的參數作為等待秒數,當秒數逾時後原本進入等待狀態的執行緒會自動恢復成執行狀態,而逾時後會回傳false。
 
       執行緒透過呼叫WaitOne這個方法來讓自己進入等待狀態,但是否進入等待狀態還要依AutoResetEvent本身是處於那種初始狀態(i.e. 終止or非終止);若AutoResetEvent本身是處於非終止狀態則執行緒呼叫WaitOne時會進入等待狀態,但AutoResetEvent為終止狀態時呼叫WaitOne卻不會進入等待狀態,但是AutoResetEvent會自動轉變成為非終止狀態,當再次呼叫WaitOne時就能夠讓執行緒進入等待狀態。
 
   2. Signal(): 這個方法是用來讓某一個因呼叫WaitOne而進入等待狀態的執行緒,從等待狀態恢復到執行狀態。
public static AutoResetEvent autoEvent = new AutoResetEvent(false);

public static void Main(string[] args)
{
   Console.WriteLine("Main thread Start at:" + DateTime.Now.ToLongTimeString());
   Thread t = new Thread(TestMethod);
   t.Start();
   Thread.Sleep(3000);
   autoEvent.Set();
   Console.Read();
}

public static void TestMethod()
{
   if(autoEvent.WaitOne(2000))
   {
      Console.WriteLine("Get Signal to Work");
      Console.WriteLine("Method restart run at:" + DateTime.Now.ToLongTimeString());
   }
   else
   {
      Console.WriteLine("Time out to work");
      Conosle.WriteLine("Method restart run at:" + DateTime.Now.ToLongTimeString());
   }
}
 
   ManualResetEvent(手動重置事件)
 
   ManualResetEvent和AutoResetEvent使用上幾乎一致,因為它們都是衍生自EventWaitHandle。不過其區別如下:
 
   1. 當AutoResetEvent為終止狀態時,呼叫WaitOne方法的執行緒不會進入等待狀態,而AutoResetEvent則本身會將狀態改變成為非終止狀態。
   2. ManualResetEvent為終止狀態時呼叫WaitOne方法的執行緒不會進入等待狀態,ManualResetEvent也不會自動進入非終止狀態。
private static ManualResetEvent manualEvent = new ManualResetEvent(true);

public static void Main(string[] args)
{
   Console.WriteLine("Main Thread Start run at:" + DateTime.Now.ToLongTimeString());
   Thread t = new Thread(TestMethod);
   t.Start();
   Console.Read();
}

public static void TestMethod()
{
   manualEvent.WaitOne();
   Console.WriteLine("Method start at:" + DateTime.Now.ToLongTimeString());
   manualEvent.WaitOne(1000);
   Console.WriteLine("Method start at:" + DateTime.Now.ToLongTimeString());
}

ManualResetEvent

完全不會阻擋,縱使設定了逾時時間也一樣。

TPL(Task Parallel Language) – 平行化處理

.Net Framework的平行化程式
   目前的個人電腦以及伺服器工作站已具備二到四核心(i.e.實體CPU),這使得多執行緒可以在同一時間點上一起被執行。在不久的未來,個人電腦將會擁有更多的核心。為了能夠妥善的利用硬體所帶來的資源,可以考慮使用平行化程式將工作分散在多個核心中。在過去,平行化需要較處理執行緒和資源鎖這種細節上的處理,自從.Net Framework 4.0開始,提供了一套新的類別庫以及探測工具來支援平行化程式撰寫。這套新的類別庫可以讓開發能夠寫出更有效率且細顆粒度和可延展平行的程式,並且不需要再對執行緒(Thread)或是執行緒池(ThreadPool)進行操作。下圖描繪了.Net Framework 4.0的平行程式架構:

IC292903
圖1. .Net Framework 4.0的平行程式架構

Task類別
   Task與Task<TResult>類別是TPL的基礎類別,而Task<TResult>為Task的泛型版本。而Task<TResult>中的TResult為Task執行完畢後回傳值的資料型別。
啟動Task
Task task = new Task (() => Console .Write("Hi" ));
task.Start();

   透過實體一個Task物件,接著再呼叫該物件的Start()方法。而Task類別的建構子除了上述例子輸入型別是Action委派型別之外,還有另一種建構子: Task(Action, TaskCreationOptions);而TaskCreationOptions是一個列舉型別,其中較常被使用到的為: LongRunning,該列舉值是在當反覆測試過後判斷這個Task較適合採用長時間執行的方式來運行才選用。TaskCreationOptions是用於告知ThreadPool這個Task不要放在ThreadPool中執行,否則一般的Task都是在ThreadPool中以非同步方式執行。

   上述啟動Task的方式較傳統與一般,但是在實務中會採用更簡便的手法:
Task.Run(() => Console .WriteLine("Hi" ));

   以上述方式即可直接啟動Task而不需實體化Task物件。Task.Run是Task類別的靜態方法,輸入參數是Action委派型別,而Run()的回傳值回一個Task物件。除了Task.Run(Action)之外,Task的Run還有另一種泛型版本:
Task.Run<TResult>( Func<Task<TResult>>);

這個版本的輸入參數由Action委派型別換成是Func<Task<TResult>> ,其範例如下:
Task<int > task=Task .Run<int >(() => { return 1 + 1; });
int result = task.Result;

Task的等待

   一般狀況下,Task是ThreadPool以非同步方式執行,系統想要知道這個Task是否已經執行完畢可以透過查看Task物件的屬性IsCompleted來判斷,亦可以呼叫Task物件的方法:Wait()。
   但是若是採用Wait()方法的方式會令主執行緒暫停。
Task< int> task= Task.Run< int>(() => { return 1 + 1; });
int result = default( int);

if(task.IsCompleted)
     result=task.Result;

Task< int> task= Task.Run< int>(() => { return 1 + 1; });
task.Wait();
int result = task.Result;

   Wait()這個方法還有其它多載: bool Wait(int millisecondsTimeout),該重載方法傳入一個毫秒為單位的整數,Task的Wait為依此傳入的秒數作為等待時間的依據,若在時間內Task執行完畢則回傳True,反之,則是False。

Task< int> task = Task.Run< int>(() => { Thread.Sleep(5000); return 1 + 1; });
bool blOver=task.Wait(4900);
int result = task.Result;

Task的串聯

   Task串聯指的是當一個Task執行完畢之後接著執行另一個Task,通常有兩種方法來達成。
   1. Task物件.GetAwaiter()
   2. Task物件.ContinueWith(Action)

   採用第1種方法,當呼叫GetAwaiter()方法後,會回傳TaskAwaiter結構體,並且該TaskAwaiter結構體具有一個OnCompleted方法,而Task執行完畢後的下一個Task所要執行的動作就是藉由輸入這個物件的OnCompleted事件來達成:
Task< int> task = Task.Run< int>(() => { Thread.Sleep(5000); return 1 + 1; });
TaskAwaiter< int> awaiter = task.GetAwaiter();
awaiter.OnCompleted(() => {
   Console.WriteLine( "hi");
});

第2種方式較常使用,而這種方式和第1種不同的地方在於當發生異常時,第1種方式可以直接截取,而第二種僅能在呼叫Result屬性才會拋出異常,並且會將異常包裹成AggregateException,但第1種的異常不會包裹。
Task< int> task = Task.Run< int>(() => { Thread.Sleep(5000); return 1 + 1; });
Task task2 = task.ContinueWith(o => Console.WriteLine( "hi"));
bool blOver=task.Wait(4900);
int result = task.Result;
Console.WriteLine(blOver);

異常的處理

   和Thread類別不同的地方是 Task拋出的異常是可以被截取,但也不是直接就能截取到,而是藉由呼叫Wait()時或是Result屬性時Task才會拋出異常,並且這個異常會被包裹成AggregateException異常型別。

   實務上,Task的啟動多半都是採用Task.Run(Action)或是
Task.Run<TResult( Func<Task<TResult>>)這兩種方式,在某些情境下不需要關心它們的執行是否已完成,但是對於它們在執行中是否有發生異常還是需要加以關注,而這些異常亦即未觀察到的異常(Unobserved Exception),可以透過註冊一個全域的靜態事件: TaskScheduler.UnobservedTaskException來處理這些異常;當GC要回收Task物件時,若在此之前有某個Task執行發生了異常,則在GC回收之前會執行註冊UnobservedTaskException事件的方法。
TaskScheduler.UnobservedTaskException += (obj, e) => Console.WriteLine(e.Exception.Message);
Task< int> task = Task.Run< int>(() => { Thread.Sleep(5000); return 1 + 1; });

TaskCompletionSource

   TaskCompletionSource是一種承載需要長時間執行工作的Task管理類別,該類別較常使用的有三個方法:
   1. SetResult: 承載最後處理的結果
   2. SetException: 承載異常
   3. SetCanceled: 承載該工作已被取消
   而其較簡易的使用方式如下:
TaskCompletionSource<int > tcs = new TaskCompletionSource <int >();
Task.Run(
   () => {
      Thread.Sleep(5000);
      int i = Enumerable.Range(0, 100).Sum();
      tcs.SetResult(i);
});

Task< int> task = tcs.Task;

TaskCompletionSource的Task屬性即為包裹後所產出的Task物件,接下來即可操作Task物件。依上述程式碼來看TaskCompletionSource類別的執行結果需待Task.Run方法中的工作執行完畢才能取得,這是一種變相的執行緒同步手法,並且最終取得執行的成果。
   TaskCompletionSource在實務應用上可用來作為延遲執行;通常可配合Timer來處理:
TaskCompletionSource<int > tcs = new TaskCompletionSource <int >();
System.Timers. Timer timer = new System.Timers. Timer(5000) { AutoReset = false };

timer.Elapsed += (sender, e) => {
      timer.Dispose();
      //執行某段工作
      tcs.SetResult(result);
};

timer.Start();
Task< int> task = tcs.Task;
int  finalResult= task.Result;

   除了延遲執行外,TaskCompletionSource在處理事件驅動程式(EAP)的重構方面亦大有助益;實務中,EAP很容易讓UI的控制項與商業邏輯混合在一起造成維護上的困擾,故可以藉TaskCompletionSource包裹一些工作,並且再藉由TaskCompletionSource最後產出的Task物件來達成UI與商業邏輯切割。
(重構前)
WebClient wc = new WebClient();

wc.DownloadStringCompleted += (sender, e) => {
   if (e.Cancelled)
      Console.WriteLine( "Cancel");
   else if (e.Error != null)
      Console.WriteLine( "Error");
   else
      Console.WriteLine(e.Result);
};

(重構後)
TaskCompletionSource<string > tcs = new TaskCompletionSource<string >();
WebClient wc = new WebClient();

wc.DownloadStringCompleted += (sender, e) => {
   if (e.Cancelled)
       tcs.SetCanceled();
   else if (e.Error != null)
       tcs.SetException(e.Error);
   else
       tcs.SetResult(e.Result);
};

Task< string> task = tcs.Task;
string strResult= task.Result;
Console.WriteLine(strResult);

   TaskCompletionSource除了在EAP上的應用之外,亦可應用在IAsyncResult模式下:
非同步方法簽名如下:
public static IAsyncResult BeginGetHostAddresses(string hostNameOrAddress, AsyncCallback requestCallback, Object state);

public static IPAddress[] EndGetHostAddresses( IAsyncResult asyncResult);

(重構前)
static void Main()
{
   Dns.BeginGetHostAddresses( "www.yahoo.com", result =>
   {
      IPAddress[] address = Dns.EndGetHostAddresses(result);
      Console.WriteLine(addresses[0]);
   }, null);

   Console.ReadKey();
}

(重構後)
public static Task< IPAddress[]> GetHostAddressesAsTask(string hostNameOrAddress)
{
   var tcs = new TaskCompletionSource<IPAddress []>();
   Dns.BeginGetHostAddresses(hostNameOrAddress, iar =>
   {
      try
      {
         tcs.SetResult( Dns.EndGetHostAddresses(iar));
      }
      catch ( Exception ex)
      {
         tcs.SetException(ex);
      }
   }, null);

   return tcs.Task;
}

大量資料平行化處理

   使用時機: 當集合中所有的元素都有相同的資料操作行為需要同步進行時。集合會被拆分成多個區塊,而不同執行緒可以在同一時間處理各自被分派的區塊。TPL(Task Parallel Library)藉由System.Threading.Tasks.Parallel提供資料平行化處理。這個類別提供For/ForEach以方法導向的實做方式來達到平行化。開發時,撰寫Parallel.For/Parallel.ForEach的迴圈內容就如同在撰寫一般的For/ForEac的迴圈內容,完全不需要處理執行緒或是佇列中的工作項目。
Parallel.ForEach(sourceCollection, item => Process(item));

   當平行化迴圈開始執行時,TPL中的Task scheduler會依硬體資源以及工作負載將sourceCollection切割成數份,Task Scheduler會自動調節工作量,當它認知到工作負載不平衡的時候,可能會使用到更多的執行緒和CPU核心數。

   Parallel.For/Parallel.ForEach提供了多個函式的負載,讓開發人員能夠:監控其它執行緒的狀態、中斷迴圈執行、維護執行緒的本地狀態、釋放執行緒本地狀態、控制平行化的程度...等等。能夠達成這些操作的相關支援類別如下:
   1. ParallelLoopState
   2. ParallelOptions
   3. ParallelLoopResult
   4. CancellationToken
   5. CancellationTokenSource

大量資料平行化處理-For

   在使用Parallel.For時,需注意是否真的需要使用到平行化的For;若For迴圈內的工作其所需資源與複雜度並不高,則可以考慮不使用平行化的Parallel.For而改使用一般的For。
   Parallel.For與一般的For迴圈在撰寫上相當類似,不同的地方在於Parallel.For中反覆執行的是一個"方法",而一般的For迴圈則是一段Script;因此,雖說寫法相當類似但是實際上並不全然相同,譬如說,在一般的For迴圈中若在滿足了一定的條件後中斷(Break),僅需在For迴圈中撰寫if判斷式,並且滿足判斷式後執行break這個指令即可中斷For迴圈的執行,但Parallel.For執行break指令是無任何意義的,因此,要處理一般For迴圈中所能夠達到的邏輯,需要借助ParallelLoopState物件來處理。
Parallel.For( 0, 100, (i, loopState)=>
{
   if( i>90)
      loopState.Break();
}

   當i大於90時即刻中斷尚未執行完的所有工作,實際執行時有可能第二個執行緒執行時其值就超過90了,因此,不能預設大約會執行近90次才會中斷。Parallel.For本身的回傳值為PrallelLoopResult結構體,此為一個用於判斷是否所有工作都已經執行完畢,通常會和需要中斷條件的現實需求相互搭配使用,讓開發人員能夠在程式中取得Parallel.For中斷之後關於Parallel.For的執行狀況,例如:是否所有工作都執行完畢(IsCompleted)、最小的上下界索引值(LowestBreakIteration)為何。

上例是基本較常使用到的方法重載。除了這個重載外Parallel.For還有多達12個重載。不同的重載其差別大多是起始和終止的上下界參數資料型別為int或long的差別,除此之外,Parallel還允許更多的控制,諸如執行緒的狀態物件設定以及平行化執行的組態設定等等。
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = 2;
Parallel.For(0, 9, options, (i)=>{
     // DoSomething
});

   上例中使用Parallel.For另一個重載的方法,並且設定其執行時的平行度;當設定平行度為-1時,代表無執行緒的個數限制,設定為1則代表不使用平行化以循序方式執行。
大量資料平行化處理-ForEach

Parallel.ForEach在程式撰寫上十分類似Parallel.For。來源資料集合被切割成數份並且依照系統的硬體資源

使用Task Parallel的陷阱

   Parallel.For與Parallel.ForEach在大多數狀況下針對循序式的迴圈效能能夠有大幅度的改善。但是導入平行處理後勢必也帶來了一定的複雜性,而這些複雜性很可能會造成一些問題。以下接著討論可能的問題:

   1. 不要假設使用了平行處理就一定能加速
       在某些狀況下,平行迴圈可能會比循序式迴圈處理還要慢;使用平行迴圈的最高指導原則就是僅有較少的迭代,但執行複雜度低的委派內容反而會造成效能不彰。由於,有太多可能會影響到效能的因素,是故,建議使用時一定要效能測試。

   2. 避免在委派中撰寫到記憶體區塊共享的程式碼
       平行處理在存取靜態變數這類的共享記憶體型變數與一般的非平行處理程式有所不同。因此,當有大量的執行緒在存取共享記憶體區塊的變數時,勢必會有競爭現象。雖然解決方式為對需要同步化存取的變數進行鎖定,但這也造成了效能上的損害。因此,建議盡量避免撰寫到需要存取共享記憶體區塊的變數,至少也要有所節制。在平行處理中難免會需要存取執行緒的狀態,而最佳解決方案則是使用System.Threading.threadLocal<T>型別的變數來儲存執行緒內部的變數。例如:求取總合時就需要使用一個變數不斷累加數值:
long total = 0;
Parallel.For< long>(0, array.Length, () => 0, (index, loop, subtotal) =>
{
   subtotal += array[index];
   return subtotal;
}, (x) => Interlocked.Add( ref total, x));

   3. 避免過度平行化
       使用平行化迴圈本質是會附帶一些額外的資料切割以及同步化工作執行緒的成本。而平行化所能產生的效能提速是基於所能使用的伺服器CPU核心數。因此,如果僅有一顆核心的伺服器無法讓平行化加速,是故,要注意不要所有大量資料處理都使用平行化。大抵上,平行化多半是用在巢狀迴圈中的最外層迴圈,僅有在下列狀況下才會在內層迴圈中使用:
       3.1. 內部迴圈的內容其執行動作的複雜度高
       3.2. 內部迴圈的內容其執行動作的執行較長
       3.3. 執行系統的硬體具有足夠的核心數來處理夠多數量的執行緒

   4. 避免呼叫非執行緒保護的方法
       在Parallel.For或Parallel.ForEach的委派中呼叫非執行緒保護的方法可能會造成非預期的執行的動作或是異常(Exception)。

   5. 限制執行緒保護方法的呼叫次數
       大多數在.Net Framework中的靜態方法都是屬於執行緒保護的方法,並且能夠讓多執行緒同時呼叫。然而,這會為了達到同步化而帶來重大的效能低落。

   6. 留心執行緒混搭議題
       某些技術,例如COM與STA或WindowForm,WPF等前端技術混搭時,COM必須執行在指定的執行緒上,而WindowForm或WPF的控制項只能由創建它的執行緒可以對其進行操作,這意味著,你無法在COM所在的執行緒中試圖更新控制項,除非有設定執行緒排程:
Task t1 = new Task(() =>
{
   //DoSomething
});

var UISyncContext = TaskScheduler.FromCurrentSynchronizationContext();

Task t2 = t1.ContinueWith((lastResult) =>
{
   //UI控制項設定
}, UISyncContext);

   7. 謹慎使用Parallel.Invoke的等待委派
       在某些情境下,Task會是執行在某條執行緒中。這種效能優化方式很有可能會造成死結。舉例來說,當兩個Task執行同一段委派程式碼時,當這段委派程式碼中使用EventWaitHandler進行程式碼的執行權管理時,若第一個Task在執行之後沒有通知另一個正在等待中的Task,就會造成死結。解決的方法可以在等待委派中指定等待逾時時間,或是在創建這條執行兩個Task的執行緒建構子中設定執行時內部的Task彼此之間不會阻斷。

   8. 不要假設ForEach/For/ForAll<TSource>的迭代永遠都應該平行處理
       其實ForEach/For/ForAll並非一定非要以平行化來處理資料。因此,應該避免在內部程式碼中以多執行緒的邏輯撰寫程式;不需要加上鎖或是事件通知(EventWaitHandler)等機制。

   9. 避免將平行化迴圈寫在UI執行緒中
       若希望在大量資料處理完畢之後將結果顯示在UI上,考慮將平行化迴圈寫在背景執行緒中,並且在執行完畢後呼叫UI執行緒進行結果的顯示。這麼做是為了避免發生系統異常,因為多執行緒允許操作UI上的控制項時,很可能會因為搶占控制項或是等待更甚是死結的現象發生。
(有問題的程式碼)
public void btnOk_Click( object sender, EventArgs e)
{
   int N=100;
   Parallel.For(0, N, i=>
   {
      btnOk.Invoke(( Action) delegate { DisplayProgress(i) });
   });
}

(改善後)
public void btnOk_Click(object sender, EventArgs e)
{
   Task.Factory.StartNew(() =>
   Parallel.For(0, N, i =>
   {
      btnOk.Invoke(( Action) delegate { DisplayProgress(i);});
   });
}

參考項目
1. http://cnn237111.blog.51cto.com/2359144/1102476