菜鳥學習並行編程,參考《C#並行編程高級教程.PDF》,如有錯誤,歡迎指正。
背景
有時候必須訪問變量、實例、方法、屬性或者結構體,而這些並沒有准備好用於並發訪問,或者有時候需要執行部分代碼,而這些代碼必須單獨運行,這是不得不通過將任務分解的方式讓它們獨立運行。
當任務和線程要訪問共享的數據和資源的時候,您必須添加顯示的同步,或者使用原子操作或鎖。
之前的.NET Framework提供了昂貴的鎖機制以及遺留的多線程模型,新的數據結構允許細粒度的並發和並行化,並且降低一定必要的開銷,這些數據結構稱為輕量級同步原語。
這些數據結構在關鍵場合下能夠提供更好的性能,因為它們能夠避免昂貴的鎖機制,如果在等待時間不短的情況下使用它們,這些原語會增加額外的開銷。
如果您需要特定的執行順序,可以通過添加顯示同步來實現。
同步原語
.NET Framework 4在現在的System.Threading命名空間中提供了6個同步原語,通過這個命名空間可以訪問遺留的線程類、類型和枚舉,還提供了新的基於任務的編程模型及特定情形緊密相關的數據結構
Barrier 使多個任務能夠采用並行方式依據某種算法在多個階段中協同工作 通過屏障
CountdownEvent 表示在計數變為0時處於有信號狀態的同步基元 通過信號機制
ManualResetEventSlim 允許很多任務等待直到另一個任務手工發出事件句柄,當預計等待時間很短的時候,ManualResetEventSlim 的性能比對應的重量級ManualResetEvent的性能要高。通過信號機制
SemaphoreSlim 限制對可同時訪問資源或資源池的線程數,比對應的Semaphore性能要高 通過信號機制
SpinLock 提供一個相互排斥鎖基元,在該基元中,嘗試獲得鎖的線程將在重復檢查的循環中等待,直至該鎖變為可用為止。
SpinWait 提供對基於自旋的等待的支持。
通過屏障同步並發任務 Barrier
當在需要一組任務並行地運行一連串的階段,但是每一個階段都要等待其他任務完成前一階段之後才能開始時,您可以通過使用Barrier類的實例來同步這一類協同工作,通過屏障
下面貼代碼方便大家理解,如有問題,請指正,詳情見注釋:
class Program { private static Task[] _CookTasks { get; set; } private static Barrier _barrier { get; set; } /*獲取當前計算機處理器數*/ private static int _particpants = Environment.ProcessorCount; /* coder:釋迦苦僧 * 代碼中 展示煮飯的步驟 1.打水 2.淘米 3.放入鍋中 4.蓋上鍋蓋 5.生火煮飯 */ static void Main(string[] args) { Console.WriteLine("定義{0}個人煮飯3次", _particpants); _CookTasks = new Task[_particpants]; _barrier = new Barrier(_particpants, (barrier) => { Console.WriteLine("當前階段:{0}", barrier.CurrentPhaseNumber); }); Stopwatch swTask1 = new Stopwatch(); swTask1.Start(); /*定義N個人*/ for (int cook_person = 0; cook_person < _particpants; cook_person++) { _CookTasks[cook_person] = Task.Factory.StartNew((num) => { int index = Convert.ToInt32(num); /*每個人煮3次飯*/ for (int cook_count = 0; cook_count < 3; cook_count++) { CookStepTask1(index, cook_count); CookStepTask2(index, cook_count); CookStepTask3(index, cook_count); CookStepTask4(index, cook_count); CookStepTask5(index, cook_count); } }, cook_person); } /*ContinueWhenAll 提供一組任務完成後 延續方法*/ var finalTask = Task.Factory.ContinueWhenAll(_CookTasks, (tasks) => { /*等待任務完成*/ Task.WaitAll(_CookTasks); swTask1.Stop(); Console.WriteLine("采用並發 {1}個人煮3次飯耗時:{0}", swTask1.ElapsedMilliseconds, _particpants); /*釋放資源*/ _barrier.Dispose(); }); Thread.Sleep(4000); Stopwatch swTask = new Stopwatch(); swTask.Start(); /*定義N個人*/ for (int cook_person = 0; cook_person < _particpants; cook_person++) { /*每個人煮3次飯*/ for (int cook_count = 0; cook_count < 3; cook_count++) { CookStep1(cook_person, cook_count); CookStep2(cook_person, cook_count); CookStep3(cook_person, cook_count); CookStep4(cook_person, cook_count); CookStep5(cook_person, cook_count); } } swTask.Stop(); Console.WriteLine("不采用並發 {1}個人煮3次飯耗時:{0}", swTask.ElapsedMilliseconds, _particpants); Thread.Sleep(2000); Console.ReadLine(); } /*1.打水*/ private static void CookStepTask1(int pesron_index, int index) { Console.WriteLine("{0} 第{1}次 打水... 耗時2分鐘", pesron_index, index); Thread.Sleep(200); /*存在線程暫停 所以需要將 _barrier.SignalAndWait();放在方法中 */ _barrier.SignalAndWait(); } /*2.淘米*/ private static void CookStepTask2(int pesron_index, int index) { Console.WriteLine("{0} 第{1}次 淘米... 耗時3分鐘", pesron_index, index); Thread.Sleep(300); /*存在線程暫停 所以需要將 _barrier.SignalAndWait();放在方法中 */ _barrier.SignalAndWait(); } /*3.放入鍋中*/ private static void CookStepTask3(int pesron_index, int index) { Console.WriteLine("{0} 第{1}次 放入鍋中... 耗時1分鐘", pesron_index, index); Thread.Sleep(100); /*存在線程暫停 所以需要將 _barrier.SignalAndWait();放在方法中 */ _barrier.SignalAndWait(); } /*4.蓋上鍋蓋*/ private static void CookStepTask4(int pesron_index, int index) { Console.WriteLine("{0} 第{1}次 蓋上鍋蓋... 耗時1分鐘", pesron_index, index); Thread.Sleep(100); /*存在線程暫停 所以需要將 _barrier.SignalAndWait();放在方法中 */ _barrier.SignalAndWait(); } /*5.生火煮飯*/ private static void CookStepTask5(int pesron_index, int index) { Console.WriteLine("{0} 第{1}次 生火煮飯... 耗時30分鐘", pesron_index, index); Thread.Sleep(500); /*存在線程暫停 所以需要將 _barrier.SignalAndWait();放在方法中 */ _barrier.SignalAndWait(); } /*1.打水*/ private static void CookStep1(int pesron_index, int index) { Console.WriteLine("{0} 第{1}次 打水... 耗時2分鐘", pesron_index, index); Thread.Sleep(200); } /*2.淘米*/ private static void CookStep2(int pesron_index, int index) { Console.WriteLine("{0} 第{1}次 淘米... 耗時3分鐘", pesron_index, index); Thread.Sleep(300); } /*3.放入鍋中*/ private static void CookStep3(int pesron_index, int index) { Console.WriteLine("{0} 第{1}次 放入鍋中... 耗時1分鐘", pesron_index, index); Thread.Sleep(100); } /*4.蓋上鍋蓋*/ private static void CookStep4(int pesron_index, int index) { Console.WriteLine("{0} 第{1}次 蓋上鍋蓋... 耗時1分鐘", pesron_index, index); Thread.Sleep(100); } /*5.生火煮飯*/ private static void CookStep5(int pesron_index, int index) { Console.WriteLine("{0} 第{1}次 生火煮飯... 耗時30分鐘", pesron_index, index); Thread.Sleep(500); } } class Product { public string Name { get; set; } public string Category { get; set; } public int SellPrice { get; set; } } View Code如代碼所示,在串行代碼中,雖然任務是有序進行,但是等待的時間很長,因為只是在一個處理器下進行處理,如下圖所示:
而采用並發處理中,使用 Barrier,不僅保證了任務的有序進行,還在性能損耗上得到了最大程度的降低,如下圖
/*ContinueWhenAll 提供一組任務完成後 延續方法*/
var finalTask = Task.Factory.ContinueWhenAll(_CookTasks, (tasks) =>
{
/*等待任務完成*/
Task.WaitAll(_CookTasks);
swTask1.Stop();
Console.WriteLine("采用並發 {1}個人煮3次飯耗時:{0}", swTask1.ElapsedMilliseconds, _particpants);
/*釋放資源*/
_barrier.Dispose();
});
View Code
通過屏障同步並發任務 Barrier 下的異常和超時處理 廢話不多說 直接貼代碼,如有問題請指正: 如代碼所示,在 CookStepTask1 方法體中,我模擬了超時和異常,並在Task任務中,利用Barrier的SignalAndWait方法處理屏障中的超時信息,和Task中異常記錄信息。 鎖的特性 互斥和可見性。互斥指的是一次只允許一個線程持有某個特定的鎖,因此可以保證共享數據內容的一致性; 可見性指的是必須確保鎖被釋放之前對共享數據的修改,隨後獲得鎖的另一個線程能夠知道該行為。 參考http://www.cnblogs.com/lucifer1982/archive/2008/03/23/1116981.html 互斥鎖-System.Threading.Monitor 如果有一個臨界區,一次只有一個任務能夠訪問這個臨界區,但是這個臨界區需要被很多任務循環訪問,那麼使用任務延續並不是一個好的選擇,那麼另一種替換方案就是采用互斥鎖原語。 下面已操作字符串為示意,看下不采用鎖,采用傳統的LOCK和采用互斥鎖的區別 不采用任何鎖機制代碼如下: class Program
{
private static Task[] _CookTasks { get; set; }
private static object o = new object();
private static StringBuilder AppendStrUnLock = new StringBuilder();
private static StringBuilder AppendStrLock = new StringBuilder();
private static StringBuilder AppendStrMonitorLock = new StringBuilder();
/*獲取當前計算機處理器數*/
private static int _particpants = Environment.ProcessorCount;
/* coder:釋迦苦僧 */
static void Main(string[] args)
{
_CookTasks = new Task[_particpants];
Stopwatch swTask1 = new Stopwatch();
swTask1.Start();
for (int task_index = 0; task_index < _particpants; task_index++)
{
_CookTasks[task_index] = Task.Factory.StartNew((num) =>
{
Parallel.For(1, 1000, (i) =>
{
string str = "append message " + i;
lock (o)
{
AppendStrLock.Append(str);
}
});
}, task_index);
}
/*ContinueWhenAll 提供一組任務完成後 延續方法*/
var finalTask = Task.Factory.ContinueWhenAll(_CookTasks, (tasks) =>
{
/*等待任務完成*/
Task.WaitAll(_CookTasks);
swTask1.Stop();
Console.WriteLine("不采用Lock操作,字符串長度:{0},耗時:{1}", AppendStrLock.Length, swTask1.ElapsedMilliseconds);
/*釋放資源*/
});
Console.ReadLine();
}
}
View Code
采用互斥鎖代碼下: 但是System.Threading.Monitor好處是提供了些其他的方法(Lock中卻沒有),通過這些方法可以對鎖的過程有更多的控制。需要注意的是 Lock關鍵字和System.Threading.Monitor類仍然是提供互斥訪問的首選方法,不過在某些情形下,其他互斥鎖原語可能會提供更好的性能和更小的開銷,如SpinLock,Lock和System.Threading.Monitor類智能鎖定對象,即引用類型。 鎖超時 在多任務中,很多任務試圖獲得鎖從而進入臨界區,如果其中一個參與者不能釋放鎖,那麼其他所有的任務都要在Monitor.Enter的方法內永久的等待下去。Monitor.TryEnter方法則提供了超時機制,如代碼所示: 需要注意,上述代碼中,異常並沒有被捕捉到,因此每一個不能獲得鎖的任務都會出錯退出並停止執行。 System.Threading.Monitor類還提供了以下三個方法,大家可以參考MSND: 如果持有鎖的時間非常短,鎖的粒度很精細,那麼自旋鎖可以獲得比其他鎖機制更好的性能,互斥鎖System.Threading.Monitor的開銷非常大。 下述代碼展現System.Threading.Monitor和System.Threading.SpinLock的性能: 不要將SpinLock聲明為只讀字段,如果聲明為只讀字段,會導致每次調用都會返回一個SpinLock新副本,在多線程下,每個方法都會成功獲得鎖,而受到保護的臨界區不會按照預期進行串行化。 基於自旋鎖的等待-System.Threading.SpinWait 如果等待某個條件滿足需要的時間很短,而且不希望發生昂貴的上下文切換,那麼基於自旋的等待時一種很好的替換方案,SpinWait不僅提供了基本自旋功能,而且還提供了SpinWait.SpinUntil方法,使用這個方法能夠自旋直到滿足某個條件為止,此外SpinWait是一個Struct,從內存的角度上說,開銷很小。SpinLock是對SpinWait的簡單封裝。 需要注意的是:長時間的自旋不是很好的做法,因為自旋會阻塞更高級的線程及其相關的任務,還會阻塞垃圾回收機制。SpinWait並沒有設計為讓多個任務或線程並發使用,因此多個任務或線程通過SpinWait方法進行自旋,那麼每一個任務或線程都應該使用自己的SpinWait實例。 當一個線程自旋時,會將一個內核放入到一個繁忙的循環中,而不會讓出當前處理器時間片剩余部分,當一個任務或者線程調用Thread.Sleep方法時,底層線程可能會讓出當前處理器時間片的剩余部分,這是一個大開銷的操作。 因此,在大部分情況下,不要在循環內調用Thread.Sleep方法等待特定的條件滿足。 下面貼代碼,方便大家理解,如有錯誤請指正: volatile volatile關鍵字能夠保證;當這個共享變量被不同線程訪問和更新且沒有鎖和原子操作的時候,最新的值總能在共享變量中表示出來。 volatile變量可以看作是“輕量級lock”。當出於簡單編碼和可伸縮性考慮時,我們可能會選擇使用volatile變量而不是鎖機制。某些情況下,如果讀操作遠多於寫操作,也會比鎖機制帶來更高性能。 volatile變量具有“lock”的可見性,卻不具備原子特性。也就是說線程能夠自動發現volatile變量的最新值。volatile變量可以實現線程安全,但其應用有限。使用volatile變量的主要原因在於它使用非常簡單,至少比使用鎖機制要簡單的多;其次便是性能原因了,某些情況下,它的性能要優於鎖機制。此外,volatile操作不會造成阻塞。 參考:http://www.cnblogs.com/lucifer1982/archive/2008/03/23/1116981.html 大家可以看下 寫的不錯 ManualResetEventSlim ManualResetEventSlim通過封裝手動重置事件等待句柄提供了自旋等待和內核等待的組合。您可以使用這個類的實例在任務直接發送信息,並等待事件的發送。通過信號機制通知任務開始其工作。 其Set 方法將事件狀態設置為有信號,從而允許一個或多個等待該事件的線程繼續。 其 Wait()方法 阻止當前線程,直到設置了當前 ManualResetEventSlim 為止。 如果需要跨進程或者跨AppDomain的同步,那麼就必須使用ManualResetEvent,而不能使用ManualResetEventSlim。 SemaphoreSlim 有時候,需要對訪問一個紫雲或者一個資源池的並發任務或者線程的數量做限制時,采用System.Threading.SemaphoreSlim類非常有用。 該了表示一個Windows內核信號量對象,如果等待的時間非常短,System.Threading.SemaphoreSlim類帶來的額外開銷會更少,而且更適合對任務處理,System.Threading.SemaphoreSlim提供的計數信號量沒有使用Windows內核的信號量。 計數信號量:通過跟蹤進入和離開任務或線程來協調對資源的訪問,信號量需要知道能夠通過信號量協調機制所訪問共享資源的最大任務數,然後,信號量使用了一個計數器,根據任務進入或離開信號量控制區對計數器進行加減。 需要注意的是:信號量會降低可擴展型,而且信號量的目的就是如此。SemaphoreSlim實例並不能保證等待進入信號量的任務或線程的順序。 下面貼代碼,方便大家理解: class MRESDemo
{
/*code:釋迦苦僧*/
static void Main()
{
CountdownEvent cde = new CountdownEvent(3); // 創建SemaphoreSlim 初始化信號量最多計數為3次
Console.WriteLine(" InitialCount={0}, CurrentCount={1}, IsSet={2}", cde.InitialCount, cde.CurrentCount, cde.IsSet);
// Launch an asynchronous Task that releases the semaphore after 100 ms
Task t1 = Task.Factory.StartNew(() =>
{
while (true)
{
Thread.Sleep(1000);
if (!cde.IsSet)
{
cde.Signal();
Console.WriteLine(" InitialCount={0}, CurrentCount={1}, IsSet={2}", cde.InitialCount, cde.CurrentCount, cde.IsSet);
}
}
});
cde.Wait();
/*將 CurrentCount 重置為 InitialCount 的值。*/
Console.WriteLine("將 CurrentCount 重置為 InitialCount 的值。");
cde.Reset();
cde.Wait();
/*將 CurrentCount 重置為 5*/
Console.WriteLine("將 CurrentCount 重置為 5");
cde.Reset(5);
cde.AddCount(2);
cde.Wait();
/*等待任務完成*/
Task.WaitAll(t1);
Console.WriteLine("任務執行完成");
/*釋放*/
cde.Dispose();
Console.ReadLine();
}
}
View Code
class MRESDemo
{
/*code:釋迦苦僧*/
static void Main()
{
CountdownEvent cde = new CountdownEvent(3); // 創建SemaphoreSlim 初始化信號量最多計數為3次
Console.WriteLine(" InitialCount={0}, CurrentCount={1}, IsSet={2}", cde.InitialCount, cde.CurrentCount, cde.IsSet);
/*創建任務執行計數*/
Task t1 = Task.Factory.StartNew(() =>
{
for (int index = 0; index <= 5; index++)
{
/*重置計數器*/
cde.Reset();
/*創建任務執行計數*/
while (true)
{
Thread.Sleep(1000);
if (!cde.IsSet)
{
cde.Signal();
Console.WriteLine("第{0}輪計數 CurrentCount={1}", index, cde.CurrentCount);
}
else
{
Console.WriteLine("第{0}輪計數完成", index);
break;
}
}
/*等待計數完成*/
cde.Wait();
}
});
t1.Wait();
/*釋放*/
cde.Dispose();
Console.ReadLine();
}
}
View Code
關於並行編程中的線程同步原語就寫到這,如有問題,歡迎指正 作者:釋迦苦僧 出處:http://www.cnblogs.com/woxpp/p/3941550.html 本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。 ->是一個整體,它是用於指向結構體、C++中的class等含有子數據的指針用來取子數據。換種說法,如果我們在C語言中定義了一個結構體,然後申明一個指針指向這個結構體,那麼我們要用指針取出結構體中的數據,就要用到“->”. ->是一個整體,它是用於指向結構體、C++中的class等含有子數據的指針用來取子數據。換種說法,如果我們在C語言中定義了一個結構體,然後申明一個指針指向這個結構體,那麼我們要用指針取出結構體中的數據,就要用到“->”.C語言中->是什?
舉個例子:
struct Data
{
int a,b,c;
}; /*定義結構體*/
struct Data * p;/*定義結構體指針*/
struct Data A = {1,2,3};/*聲明變量A*/
int x;/*聲明一個變量x*/
p = &A ; /*讓p指向A*/
x = p->a;/*這句話的意思就是取出p所指向的結構體中包含的數據項a賦值給x*/
/*由於此時p指向A,因而 p->a == A.a,也就是1*/
對於一開始的問題 p = p->next;這應該出現在C語言的鏈表,這裡的next應該是一個與p同類型的結構體指針,其定義格式應該是:
struct Data
{
int a;
struct Data * next;
};/*定義結構體*/
…………
main()
{
struct Data * p;/*聲明指針變量p*/
……
p = p->next;/*將next中的值賦給p*/
}
鏈表指針是C語言的一個難點,但也是重點,學懂了非常有用。要仔細講就必須先講變量、指針。
什麼是變量?所謂變量,不要淺顯的認為會變得量就是變量。套用我們院長的問話:“教室變不變?”變,因為每天有不同的人在裡面上課,但又不變,因為教室始終在那,沒有變大或變小。這就是變量:有一個不變的地址和一塊可變的存儲空間。正常情況下,我們只看到變量這個房間裡面的東西,也就是其內容,但不會關注變量的地址,但是C語言的指針,就是這個房間的地址。我們聲明變量就相當於蓋了間房子存放東西,我們可以直接觀看房子裡的東西,而聲明指針,就是相當於獲得了一個定位器,當用指針指向某個變量時,就是用指針給變量定位,以後我們就可以用指針找到他所“跟蹤”的變量並可以獲得裡面的內容。
那結構體呢?結構體就相當於是有好幾個房子組成的別墅,幾個房子綁定在一起使用。假設現在有很多這種別墅分布在一個大迷宮裡,每間別墅裡都有一間房子。裡面放了另一個別墅的位置信息,現在你手拿定位器找到了第一棟別墅,從裡面得到了你想要的東西(鏈表的數據部分),然後把下一棟別墅的位置計入你的定位器(p = p->next),再走向下一棟別墅……如此走下去,知道走到某地下一棟別墅信息沒有了(p->next == NULL),你的旅行結束。這就是鏈表一次遍歷的過程。現在你能明白 p=p->next的含義了吧!
寫了這麼多。希望你能明白。
如果想學好c和C++,鏈表和指針必須熟練掌握!
C語言中->是什?
舉個例子:
struct Data
{
int a,b,c;
}; /*定義結構體*/
struct Data * p;/*定義結構體指針*/
struct Data A = {1,2,3};/*聲明變量A*/
int x;/*聲明一個變量x*/
p = &A ; /*讓p指向A*/
x = p->a;/*這句話的意思就是取出p所指向的結構體中包含的數據項a賦值給x*/
/*由於此時p指向A,因而 p->a == A.a,也就是1*/
對於一開始的問題 p = p->next;這應該出現在C語言的鏈表,這裡的next應該是一個與p同類型的結構體指針,其定義格式應該是:
struct Data
{
int a;
struct Data * next;
};/*定義結構體*/
…………
main()
{
struct Data * p;/*聲明指針變量p*/
……
p = p->next;/*將next中的值賦給p*/
}
鏈表指針是C語言的一個難點,但也是重點,學懂了非常有用。要仔細講就必須先講變量、指針。
什麼是變量?所謂變量,不要淺顯的認為會變得量就是變量。套用我們院長的問話:“教室變不變?”變,因為每天有不同的人在裡面上課,但又不變,因為教室始終在那,沒有變大或變小。這就是變量:有一個不變的地址和一塊可變的存儲空間。正常情況下,我們只看到變量這個房間裡面的東西,也就是其內容,但不會關注變量的地址,但是C語言的指針,就是這個房間的地址。我們聲明變量就相當於蓋了間房子存放東西,我們可以直接觀看房子裡的東西,而聲明指針,就是相當於獲得了一個定位器,當用指針指向某個變量時,就是用指針給變量定位,以後我們就可以用指針找到他所“跟蹤”的變量並可以獲得裡面的內容。
那結構體呢?結構體就相當於是有好幾個房子組成的別墅,幾個房子綁定在一起使用。假設現在有很多這種別墅分布在一個大迷宮裡,每間別墅裡都有一間房子。裡面放了另一個別墅的位置信息,現在你手拿定位器找到了第一棟別墅,從裡面得到了你想要的東西(鏈表的數據部分),然後把下一棟別墅的位置計入你的定位器(p = p->next),再走向下一棟別墅……如此走下去,知道走到某地下一棟別墅信息沒有了(p->next == NULL),你的旅行結束。這就是鏈表一次遍歷的過程。現在你能明白 p=p->next的含義了吧!
寫了這麼多。希望你能明白。
如果想學好c和C++,鏈表和指針必須熟練掌握!