C#完成多線程的同步辦法實例剖析。本站提示廣大學習愛好者:(C#完成多線程的同步辦法實例剖析)文章只能為提供參考,不一定能成為您想要的結果。以下是C#完成多線程的同步辦法實例剖析正文
本文重要描寫在C#中線程同步的辦法。線程的根本概念網上材料也許多就不再贅述了。直接接入 主題,在多線程開辟的運用中,線程同步是弗成防止的。在.Net框架中,完成線程同步重要經由過程以下的幾種方法來完成,在MSDN的線程指南中曾經講了幾種,這裡聯合作者現實頂用到的方法一路解釋一下。
1. 保護自在鎖(InterLocked)完成同步
2. 監督器(Monitor)和互斥鎖(lock)
3. 讀寫鎖(ReadWriteLock)
4. 體系內查對象
1) 互斥(Mutex), 旌旗燈號量(Semaphore), 事宜(AutoResetEvent/ManualResetEvent)
2) 線程池
除以上的這些對象以外完成線程同步的還可使用Thread.Join辦法。這類辦法比擬簡略,當你在第一個線程運轉時想期待第二個線程履行成果,那末你可讓第二個線程Join出去便可以了。
自在鎖(InterLocked)
對一個32位的整型數停止遞增和遞加操作來完成鎖,有人會問為何不消++或--來 操作。由於在多線程中對鎖停止操作必需是原子的,而++和--不具有這個才能。InterLocked類還供給了兩個別的的函數Exchange, CompareExchange用於完成交流和比擬交流。Exchange操作會將新值設置到變量中並前往變量的本來值: int oVal = InterLocked.Exchange(ref val, 1)。
監督器(Monitor)
在MSDN中對Monitor的描寫是: Monitor 類經由過程向單個線程授與對象鎖來掌握對對象的拜訪。
Monitor類是一個靜態類是以你不克不及經由過程實例化來獲得類的對象。Monitor 的成員可以檢查MSDN,根本上Monitor的後果和lock是一樣的,經由過程加鎖操作Enter設置臨界區,完成操作後應用Exit操作來釋放對象鎖。 不外絕對來講Monitor的功效更強,Moniter可以停止測試鎖的狀況,是以你可以掌握對臨界區的拜訪選擇,期待or分開, 並且Monitor還可以在釋放鎖之前告訴指定的對象,更主要的是應用Monitor可以逾越辦法來操作。Monitor供給的辦法很少就只要獲得鎖的方 法Enter, TryEnter;釋放鎖的辦法Wait, Exit;還有新聞告訴辦法Pulse, PulseAll。經典的Monitor操作是如許的:
// 通監督器來創立臨界區 static public void DelUser(string name) { try { // 期待線程進入 Monitor.Enter(Names); Names.Remove(name); Console.WriteLine("Del: {0}", Names.Count); Monitor.Pulse(Names); } finally { // 釋放對象鎖 Monitor.Exit(Names); } } }
個中Names是一個List, 這裡有一個小技能,假如你想聲明全部辦法為線程同步可使用辦法屬性:
// 經由過程屬性設置全部辦法為臨界區 [MethodImpl(MethodImplOptions.Synchronized)] static public void AddUser(string name) { Names.Add(name); Console.WriteLine("Add: {0}",Names.Count); }
關於Monitor的應用有一個辦法是比擬詭異的,那就是Wait辦法。在MSDN中對Wait的描寫是: 釋放對象上的鎖以便許可其他線程鎖定和拜訪該對象。
這裡提到的是先釋放鎖,那末明顯我們須要先獲得鎖,不然挪用Wait會湧現異常,所 以我們必需在Wait後面挪用Enter辦法或其他獲得鎖的辦法,如lock,這點很主要。對應Enter辦法,Monitor給出來另外一種完成 TryEnter。這兩種辦法的重要差別在因而否壅塞以後線程,Enter辦法在獲得不到鎖時,會壅塞以後線程直到獲得鎖。不外缺陷是假如永久得不到鎖那 麼法式就會進入逝世鎖狀況。我們可以采取Wait來處理,在挪用Wait時參加超不時限便可以。
if (Monitor.TryEnter(Names)) { Monitor.Wait(Names, 1000); // !! Names.Remove(name); Console.WriteLine("Del: {0}", Names.Count); Monitor.Pulse(Names); }
互斥鎖(lock)
lock症結字是完成線程同步的比擬簡略的方法,其實就是設置一個臨界區。在 lock以後的{...}區塊為一個臨界區,當進入臨界區時加互斥鎖,分開臨界區時釋放互斥鎖。MSDN對lock症結字的描寫是: lock 症結字可將語句塊標志為臨界區,辦法是獲得給定對象的互斥鎖,履行語句,然後釋放該鎖。
詳細例子以下:
static public void ThreadFunc(object name) { string str = name as string; Random rand = new Random(); int count = rand.Next(100, 200); for (int i = 0; i < count; i++) { lock (NumList) { NumList.Add(i); Console.WriteLine("{0} {1}", str, i); } } }
對lock的應用有幾點建議:對實例鎖定lock(this),對靜態變量鎖定lock(typeof(val))。lock的對象拜訪權限最好是private,不然會湧現掉去拜訪掌握景象。
讀寫鎖(ReadWriteLock)
讀寫鎖的湧現重要是在許多情形下,我們讀資本的操作要多於寫資本的操作。然則假如每 次只對資本付與一個線程的拜訪權限明顯是低效的,讀寫鎖的優勢是同時可以有多個線程對統一資本停止讀操作。是以在讀操作比寫操作多許多,而且寫操作的時光 很短的情形下應用讀寫鎖是比擬有用率的。讀寫鎖是一個非靜態類所以你在應用前須要先聲明一個讀寫鎖對象:
static private ReaderWriterLock _rwlock = new ReaderWriterLock();
讀寫鎖是經由過程挪用AcquireReaderLock,ReleaseReaderLock,AcquireWriterLock,ReleaseWriterLock來完成讀鎖和寫鎖掌握的
static public void ReaderThread(int thrdId) { try { // 要求讀鎖,假如100ms超時加入 _rwlock.AcquireReaderLock(10); try { int inx = _rand.Next(_list.Count); if (inx < _list.Count) Console.WriteLine("{0}thread {1}", thrdId, _list[inx]); } finally { _rwlock.ReleaseReaderLock(); } } catch (ApplicationException) // 假如要求讀鎖掉敗 { Console.WriteLine("{0}thread get reader lock out time!", thrdId); } } static public void WriterThread() { try { // 要求寫鎖 _rwlock.AcquireWriterLock(100); try { string val = _rand.Next(200).ToString(); _list.Add(val); // 寫入資本 Console.WriteLine("writer thread has written {0}", val); } finally { // 釋放寫鎖 _rwlock.ReleaseWriterLock(); } } catch (ApplicationException) { Console.WriteLine("Get writer thread lock out time!"); } }
假如你想在讀的時刻拔出寫操作請應用UpgradeToWriterLock和DowngradeFromWriterLock來停止操作,而不是釋放讀鎖。
static private void UpgradeAndDowngrade(int thrdId) { try { _rwlock.AcquireReaderLock(10); try { try { // 晉升讀鎖到寫鎖 LockCookie lc = _rwlock.UpgradeToWriterLock(100); try { string val = _rand.Next(500).ToString(); _list.Add(val); Console.WriteLine ("Upgrade Thread{0} add {1}", thrdId, val); } finally { // 降低寫鎖 _rwlock.DowngradeFromWriterLock(ref lc); } } catch (ApplicationException) { Console.WriteLine("{0}thread upgrade reader lock failed!", thrdId); } } finally { // 釋放本來的讀鎖 _rwlock.ReleaseReaderLock(); } } catch (ApplicationException) { Console.WriteLine("{0}thread get reader lock out time!", thrdId); } }
這裡有一點要留意的就是讀鎖和寫鎖的超時期待時光距離的設置。平日情形下設置寫鎖的期待超時要比讀鎖的長,不然會常常產生寫鎖期待掉敗的情形。
體系內查對象 互斥對象(Mutex)
互斥對象的感化有點相似於監督器對象,確保一個代碼塊在統一時辰只要一個線程在執 行。互斥對象和監督器對象的重要差別就是,互斥對象普通用於跨過程間的線程同步,而監督器對象則用於過程內的線程同步。互斥對象有兩種:一種是定名互斥; 另外一種是匿名互斥。在跨過程中應用到的就是定名互斥,一個已定名的互斥就是一個體系級的互斥,它可以被其他過程所應用,只需在創立互斥時指定翻開互斥的名 稱便可以。在.Net中互斥是經由過程Mutex類來完成。
其實關於OpenExisting函數有兩個重載版本,
Mutex.OpenExisting (String)
Mutex.OpenExisting (String, MutexRights)
關於默許的第一個函數實際上是完成了第二個函數 MutexRights.Synchronize|MutexRights.Modify操作。
因為監督器的設計是基於.Net框架,而Mutex類是體系內查對象封裝了win32的一個內核構造來完成互斥,而且互斥操作須要要求中止來完成,是以在停止過程外線程同步的時刻機能上要比互斥要好。
典范的應用Mutex同步須要完成三個步調的操作:1.翻開或許創立一個Mutex實例;2.挪用WaitOne()來要求互斥對象;3.最初挪用ReleaseMutex來釋放互斥對象。
static public void AddString(string str) { // 設置超不時限並在wait前加入非默許托管高低文 if (_mtx.WaitOne(1000, true)) { _resource.Add(str); _mtx.ReleaseMutex(); } }
須要留意的是,WaitOne和ReleaseMutex必需成對湧現,不然會招致過程逝世鎖的產生,這時候體系(.Net2.0)框架會拋出AbandonedMutexException異常。
旌旗燈號量(Semaphore)
旌旗燈號量就像一個夜總會:它有確實的容量,並被保镳掌握。一旦滿員,就沒有人能再進入,其別人必需在裡面列隊。那末在外面分開一小我後,隊頭的人便可以進入。旌旗燈號量的結構函數須要供給至多兩個參數-現有的人數和最年夜的人數。
旌旗燈號量的行動有點相似於Mutex或是lock,然則旌旗燈號量沒有具有者。隨意率性線程都可以挪用Release來釋放旌旗燈號量而不像Mutex和lock那樣須要線程獲得資本能力釋放。
class SemaphoreTest { static Semaphore s = new Semaphore(3, 3); // 以後值=3; 容量=3 static void Main() { for (int i = 0; i < 10; i++) new Thread(Go).Start(); } static void Go() { while (true) { s.WaitOne(); Thread.Sleep(100); // 一次只要個線程能被處置 s.Release(); } } } 事宜(ManualResetEvent/AutoResetEvent) < src="http://blog.csdn.net/count.aspx?ID=1857459&Type=Rank" type="text/javascript">
AutoResetEvent
一個AutoResetEvent象是一個"檢票輪盤":拔出一張通行證然後讓一個 人經由過程。"auto"的意思就是這個"輪盤"主動封閉或許翻開讓或人經由過程。線程將在挪用WaitOne落後行期待或許是壅塞,而且經由過程挪用Set操作來插 入線程。假如一堆線程挪用了WaitOne操作,那末"輪盤"就會樹立一個期待隊列。一個通行證可以來自隨意率性一個線程,換句話說隨意率性一個線程都可以經由過程訪 問AutoResetEvent對象並挪用Set來釋放一個壅塞的線程。
假如在Set被挪用的時刻沒有線程期待,那末句柄就會一向處於翻開狀況直到有線程調 用了WaitOne操作。這類行動防止了競爭前提-當一個線程還沒來得急釋放而另外一個線程就開端進入的情形。是以反復的挪用Set操作一個"輪盤"哪怕是 沒有期待線程也不會一次性的讓一切線程進入。
WaitOne操作接收一個超時參數-當產生期待超時的時刻,這個辦法會前往一個 false。當已有一個線程在期待的時刻,WaitOne操作可以指定期待照樣加入以後同步高低文。Reset操作供給了封閉"輪盤"的操作。 AutoResetEvent可以或許經由過程兩個辦法來創立: 1.挪用結構函數 EventWaitHandle wh = new AutoResetEvent (false); 假如boolean值為true,那末句柄的Set操作將在創立後主動被挪用 ;2. 經由過程基類EventWaitHandle方法 EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto); EventWaitHandle結構函數許可創立一個ManualResetEvent。人們應當經由過程挪用Close來釋放一個Wait Handle在它不再應用的時刻。當在運用法式的生計期內Wait handle持續被應用,那末假如漏掉了Close這步,在運用法式封閉的時刻也會被主動釋放。
class BasicWaitHandle { static EventWaitHandle wh = new AutoResetEvent(false); static void Main() { new Thread(Waiter).Start(); Thread.Sleep(1000); // 期待一會兒 wh.Set(); // 叫醒 } static void Waiter() { Console.WriteLine("Waiting..."); wh.WaitOne(); // 期待叫醒 Console.WriteLine("Notified"); } }
ManualResetEvent
ManualResetEvent是AutoResetEvent的一個特例。它的 分歧的地方在於在線程挪用WaitOne後不會主動的重置狀況。它的任務機制有點象是開關:挪用Set翻開並許可其他線程停止WaitOne;挪用 Reset封閉那末列隊的線程就要期待,直到下一次翻開。可使用一個帶volatile聲明的boolean字段來模仿連續休眠 - 經由過程反復檢測標記,然後休眠一小段時光。
ManualResetEvent經常被用於協助完成一個特別的操作,或許讓一個線程在開端任務前完成初始化。
線程池(Thread Pooling)
假如你的運用法式具有年夜量的線程並消費年夜量的時光壅塞在一個Wait Handle上,那末你要斟酌應用線程池(Thead pooling)來處置。線程池經由過程歸並多個Wait Handle來勤儉期待的時光。當Wait Handle被激活時,應用線程池你須要注冊一個Wait Handle到一個拜托去履行。經由過程挪用ThreadPool.RegisterWaitForSingleObject辦法:
class Test { static ManualResetEvent starter = new ManualResetEvent(false); public static void Main() { ThreadPool.RegisterWaitForSingleObject(starter,Go,"hello",-1,true); Thread.Sleep(5000); Console.WriteLine("Signaling worker..."); starter.Set(); Console.ReadLine(); } public static void Go(object data, bool timedOut) { Console.WriteLine("Started " + data); // Perform task... } }
關於Wait Handle和拜托,RegisterWaitForSingleObject接收一個"黑盒"對象並傳遞給你的拜托(就像 ParameterizedThreadStart),超時設置和boolean標記指導了封閉和輪回的要求。一切進入池中的線程都被以為是後台線程,這 就意味著它們不再由運用法式掌握,而是由體系掌握直到運用法式加入。
留意:假如這時候候挪用Abort操作,能夠會產生意想不到的情形。
你也能夠經由過程挪用QueueUserWorkItem辦法應用線程池,指定拜托並立刻被履行。這時候你不克不及在多義務情形下保留同享線程,然則可以獲得別的的利益:線程池會堅持一個線程的總容量,看成業數超越容量時主動拔出義務。
class Test { static object workerLocker = new object(); static int runningWorkers = 100; public static void Main() { for (int i = 0; i < runningWorkers; i++) { ThreadPool.QueueUserWorkItem(Go, i); } Console.WriteLine("Waiting for threads to complete..."); lock (workerLocker) { while (runningWorkers > 0) Monitor.Wait(workerLocker); } Console.WriteLine("Complete!"); Console.ReadLine(); } public static void Go(object instance) { Console.WriteLine("Started: " + instance); Thread.Sleep(1000); Console.WriteLine("Ended: " + instance); lock (workerLocker) { runningWorkers--; Monitor.Pulse(workerLocker); } } }
為了傳遞多個對象到目的辦法,你必需界說一個客戶對象並包括一切屬性或經由過程挪用異步的拜托。如Go辦法接收兩參數:
ThreadPool.QueueUserWorkItem (delegate (object notUsed) { Go (23,34); });
其他的辦法可使用異步拜托。
願望本文所述對年夜家的C#法式設計有所贊助。