C#中的多线程 - 同步基础
生活随笔
收集整理的這篇文章主要介紹了
C#中的多线程 - 同步基础
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
C#中的多線程 - 同步基礎
*?時間代表在同一線程上一次進行加鎖和釋放鎖(假設沒有阻塞)的開銷,在?Intel?Core?i7?860?上測得。 2.1Monitor.Enter?與?Monitor.Exit C#?的lock語句是一個語法糖,它其實就是使用了try?/?finally來調用Monitor.Enter與Monitor.Exit方法。下面是在之前示例中的Go方法內部所發(fā)生的事情(簡化的版本): Monitor.Enter?(_locker);try{ ??if?(_val2?!=?0)?Console.WriteLine?(_val1?/?_val2); ??_val2?=?0;}finally?{?Monitor.Exit?(_locker);?} 如果在同一個對象上沒有先調用Monitor.Enter就調用Monitor.Exit會拋出一個異常。 lockTaken?重載 剛剛所描述的就是?C#?1.0、2.0?和?3.0?的編譯器翻譯lock語句產生的代碼。 然而它有一個潛在的缺陷。考慮這樣的情況:在Monitor.Enter的實現(xiàn)內部或者在Monitor.Enter與try中間有異常被拋出(可能是因為在線程上調用了Abort,或者有OutOfMemoryException異常被拋出),這時不一定能夠獲得鎖。如果獲得了鎖,那么該鎖就不會被釋放,因為不可能執(zhí)行到try?/?finally內,這會導致鎖泄漏。 為了避免這種危險,CLR?4.0?的設計者為Monitor.Enter添加了下面的重載: public?static?void?Enter?(object?obj,?ref?bool?lockTaken); 當(且僅當)Enter方法拋出異常,鎖沒有能夠獲得時,lockTaken為false。 下邊是正確的使用方式(這就是?C#?4.0?對于lock語句的翻譯): bool?lockTaken?=?false;try{ ??Monitor.Enter?(_locker,?ref?lockTaken); ??//?你的代碼...}finally?{?if?(lockTaken)?Monitor.Exit?(_locker);?} TryEnter Monitor還提供了一個TryEnter方法,允許以毫秒或是TimeSpan方式指定超時時間。如果獲得了鎖,該方法會返回true,而如果由于超時沒有獲得鎖,則會返回false。TryEnter也可以以無參數的形式進行調用,這是對鎖進行“測試”,如果不能立即獲得鎖就會立即返回false。 類似于Enter方法,該方法在?CLR?4.0?中也被重載來接受lockTaken參數。 2.2選擇同步對象 對所有參與同步的線程可見的任何對象都可以被當作同步對象使用,但有一個硬性規(guī)定:同步對象必須為引用類型。同步對象一般是私有的(因為這有助于封裝鎖邏輯),并且一般是一個實例或靜態(tài)字段。同步對象也可以就是其要保護的對象,如下面例子中的_list字段: class?ThreadSafe{ ??List?<string>?_list?=?new?List?<string>(); ??void?Test() ??{ ????lock?(_list) ????{ ??????_list.Add?("Item?1"); ??????//?... 一個只被用來加鎖的字段(例如前面例子中的_locker)可以精確控制鎖的作用域與粒度。對象自己(this),甚至是其類型都可以被當作同步對象來使用: lock?(this)?{?...?}//?或者:lock?(typeof?(Widget))?{?...?}????//?保護對靜態(tài)資源的訪問 這種方式的缺點在于并沒有對鎖邏輯進行封裝,從而很難避免死鎖與過多的阻塞。同時類型上的鎖也可能會跨越應用程序域(application?domain)邊界(在同一進程內)。 你也可以在被?lambda?表達式或匿名方法所捕獲的局部變量上加鎖。 鎖在任何情況下都不會限制對同步對象本身的訪問。換句話說,x.ToString()不會因為其它線程調用lock(x)而阻塞,兩個線程都要調用lock(x)才能使阻塞發(fā)生。 2.3何時加鎖 簡單的原則是,需要在訪問任意可寫的共享字段(any?writable?shared?field)時加鎖。即使是最簡單的操作,例如對一個字段的賦值操作,都必須考慮同步。在下面的類中,Increment與Assign方法都不是線程安全的: class?ThreadUnsafe{ ??static?int?_x; ??static?void?Increment()?{?_x++;?} ??static?void?Assign()????{?_x?=?123;?}} 以下是線程安全的版本: class?ThreadSafe{ ??static?readonly?object?_locker?=?new?object(); ??static?int?_x; ??static?void?Increment()?{?lock?(_locker)?_x++;?} ??static?void?Assign()????{?lock?(_locker)?_x?=?123;?}} 在非阻塞同步(nonblocking?synchronization)中,我們會解釋這種需求是如何產生的,以及在這些場景下內存屏障(memory?barrier,內存柵欄,內存柵障)和Interlocked類如何提供替代方法進行鎖定。 2.4鎖與原子性 如果一組變量總是在相同的鎖內進行讀寫,就可以稱為原子的(atomically)讀寫。假定字段x與y總是在對locker對象的lock內進行讀取與賦值: lock?(locker)?{?if?(x?!=?0)?y?/=?x;?} 可以說x和y是被原子的訪問的,因為上面的代碼塊無法被其它的線程分割或搶占。如果被其它線程分割或搶占,x和y就可能被別的線程修改導致計算結果無效。而現(xiàn)在?x和y總是在相同的排它鎖中進行訪問,因此不會出現(xiàn)除數為零的錯誤。 在lock鎖內拋出異常將打破鎖的原子性,考慮如下代碼: decimal?_savingsBalance,?_checkBalance;void?Transfer?(decimal?amount){ ??lock?(_locker) ??{ ????_savingsBalance?+=?amount; ????_checkBalance?-=?amount?+?GetBankFee(); ??}} 如果GetBankFee()方法內拋出異常,銀行可能就要損失錢財了。在這個例子中,我們可以通過更早的調用GetBankFee()來避免這個問題。對于更復雜情況,解決方案是在catch或finally中實現(xiàn)“回滾(rollback)”邏輯。 指令原子性是一個相似但不同的概念:?如果一條指令可以在?CPU?上不可分割地執(zhí)行,那么它就是原子的。(見非阻塞同步) 2.5嵌套鎖 線程可以用嵌套(重入)的方式重對相同的對象進行加鎖: lock?(locker) ??lock?(locker) ????lock?(locker) ????{ ???????//?...????} 或者: Monitor.Enter?(locker);? Monitor.Enter?(locker);? Monitor.Enter?(locker); Monitor.Exit?(locker); Monitor.Exit?(locker); Monitor.Exit?(locker); 在這樣的場景中,只有當最外層的lock語句退出或是執(zhí)行了匹配數目的Monitor.Exit語句時,對象才會被解鎖。 嵌套鎖可以用于在鎖中調用另一個方法(也使用了同一對象來鎖定): static?readonly?object?_locker?=?new?object(); static?void?Main(){ ??lock?(_locker) ??{ ?????AnotherMethod(); ?????//??這里依然擁有鎖,因為鎖是可重入的??}}static?void?AnotherMethod(){ lock?(_locker)?{?Console.WriteLine?("Another?method");?}} 線程只會在第一個(最外層)lock處阻塞。 2.6死鎖 當兩個線程等待的資源都被對方占用時,它們都無法執(zhí)行,這就產生了死鎖。演示死鎖最簡單的方法就是使用兩個鎖: object?locker1?=?new?object();object?locker2?=?new?object();new?Thread?(()?=>?{ ????????????????????lock?(locker1) ????????????????????{ ??????????????????????Thread.Sleep?(1000); ??????????????????????lock?(locker2);??????//?死鎖????????????????????} ??????????????????}).Start();lock?(locker2){ ??Thread.Sleep?(1000); ??lock?(locker1);??????????????????????????//?死鎖} 更復雜的死鎖鏈可能由三個或更多的線程創(chuàng)建。 在標準環(huán)境下,CLR?不會像SQL?Server一樣自動檢測和解決死鎖。除非你指定了鎖定的超時時間,否則死鎖會造成參與的線程無限阻塞。(在SQL?CLR?集成宿主環(huán)境中,死鎖能夠被自動檢測,并在其中一個線程上拋出可捕獲的異常。) 死鎖是多線程中最難解決的問題之一,尤其是在有很多關聯(lián)對象的時候。這個困難在根本上在于無法確定調用方(caller)已經擁有了哪些鎖。 你可能會鎖定類x中的私有字段a,而并不知道調用方(或者調用方的調用方)已經鎖住了類y中的字段b。同時,另一個線程正在執(zhí)行順序相反的操作,這樣就創(chuàng)建了死鎖。諷刺的是,這個問題會由于(良好的)面向對象的設計模式而加劇,因為這類模式建立的調用鏈直到運行時才能確定。 流行的建議:“以一致的順序對對象加鎖以避免死鎖”,盡管它對于我們最初的例子有幫助,但是很難應用到剛才所描述的場景。更好的策略是:如果發(fā)現(xiàn)在鎖區(qū)域中的對其它類的方法調用最終會引用回當前對象,就應該小心,同時考慮是否真的需要對其它類的方法調用加鎖(往往是需要的,但是有時也會有其它選擇)。更多的依靠聲明方式(declarative)與數據并行(data?parallelism)、不可變類型(immutable?types)與非阻塞同步構造(?nonblocking?synchronization?constructs),可以減少對鎖的需要。 有另一種思路來幫助理解這個問題:當你在擁有鎖的情況下訪問其它類的代碼,對于鎖的封裝就存在潛在的泄露。這不是?CLR?或?.NET?Framework?的問題,而是因為鎖本身的局限性。鎖的問題在許多研究項目中被分析,包括軟件事務內存(Software?Transactional?Memory)。 另一個死鎖的場景是:如果已擁有一個鎖,在調用Dispatcher.Invoke(在?WPF?程序中)或是Control.Invoke(在?Windows?Forms?程序中)時,如果?UI?恰好要運行等待同一個鎖的另一個方法,就會在這里發(fā)生死鎖。這通常可以通過調用BeginInvoke而不是Invoke來簡單的修復。或者,可以在調用Invoke之前釋放鎖,但是如果是調用方獲得的鎖,那么這種方法可能并不會起作用。我們在富客戶端應用與線程親和中來解釋Invoke和BeginInvoke。 2.7性能 鎖是非常快的,在一個?2010?時代的計算機上,沒有競爭的情況下獲取并釋放鎖一般只需?20?納秒。如果存在競爭,產生的上下文切換會把開銷增加到微秒的級別,并且線程被重新調度前可能還會等待更久的時間。如果需要鎖定的時間很短,那么可以使用自旋鎖(SpinLock)來避免上下文切換的開銷。 如果獲取鎖后保持的時間太長而不釋放,就會降低并發(fā)度,同時也會加大死鎖的風險。 2.8互斥體(Mutex) 互斥體類似于?C#?的lock,不同在于它是可以跨越多個進程工作。換句話說,Mutex可以是機器范圍(computer-wide)的,也可以是程序范圍(application-wide)的。 沒有競爭的情況下,獲取并釋放Mutex需要幾微秒的時間,大約比lock慢?50?倍。 使用Mutex類時,可以調用WaitOne方法來加鎖,調用ReleaseMutex方法來解鎖。關閉或銷毀Mutex會自動釋放鎖。與lock語句一樣,Mutex只能被獲得該鎖的線程釋放。 跨進程Mutex的一種常見的應用就是確保只運行一個程序實例。下面演示了這是如何實現(xiàn)的: class?OneAtATimePlease{ ??static?void?Main() ??{ ????//?命名的?Mutex?是機器范圍的,它的名稱需要是唯一的????//?比如使用公司名+程序名,或者也可以用?URL????using?(var?mutex?=?new?Mutex?(false,?"oreilly.com?OneAtATimeDemo")) ????{ ??????//?可能其它程序實例正在關閉,所以可以等待幾秒來讓其它實例完成關閉 ??????if?(!mutex.WaitOne?(TimeSpan.FromSeconds?(3),?false)) ??????{ ????????Console.WriteLine?("Another?app?instance?is?running.?Bye!"); ????????return; ??????} ??????RunProgram(); ????} ??} ??static?void?RunProgram() ??{ ????Console.WriteLine?("Running.?Press?Enter?to?exit"); ????Console.ReadLine(); ??}} 如果在終端服務(Terminal?Services)下運行,機器范圍的Mutex默認僅對于運行在相同終端服務器會話的應用程序可見。要使其對所有終端服務器會話可見,需要在其名字前加上Global\。 2.9信號量(Semaphore) 信號量類似于一個夜總會:它具有一定的容量,并且有保安把守。一旦滿員,就不允許其他人進入,這些人將在外面排隊。當有一個人離開時,排在最前頭的人便可以進入。這種構造最少需要兩個參數:夜總會中當前的空位數以及夜總會的總容量。 容量為?1?的信號量與Mutex和lock類似,所不同的是信號量沒有“所有者”,它是線程無關(thread-agnostic)的。任何線程都可以在調用Semaphore上的Release方法,而對于Mutex和lock,只有獲得鎖的線程才可以釋放。 SemaphoreSlim是?Framework?4.0?加入的輕量級的信號量,功能與Semaphore相似,不同之處是它對于并行編程的低延遲需求做了優(yōu)化。在傳統(tǒng)的多線程方式中也有用,因為它支持在等待時指定取消標記?(cancellation?token)。但它不能跨進程使用。 在Semaphore上調用WaitOne或Release會產生大概?1?微秒的開銷,而SemaphoreSlim產生的開銷約是其四分之一。 信號量在有限并發(fā)的需求中有用,它可以阻止過多的線程同時執(zhí)行特定的代碼段。在下面的例子中,五個線程嘗試進入一個只允許三個線程進入的夜總會: class?TheClub{ ??static?SemaphoreSlim?_sem?=?new?SemaphoreSlim?(3);????//?容量為?3 ??static?void?Main() ??{ ????for?(int?i?=?1;?i?<=?5;?i++)?new?Thread?(Enter).Start?(i); ??} ??static?void?Enter?(object?id) ??{ ?????_sem.Wait();//?同時只能有?? Thread.Sleep?(1000?*?(int)?id);???????????????//?3個線程?? _sem.Release(); ??}} 如果Sleep語句被替換為密集的磁盤?I/O?操作,由于Semaphore限制了過多的并發(fā)硬盤活動,就可能改善整體性能。 類似于Mutex,命名的Semaphore也可以跨進程使用。 3線程安全 說一個程序或方法是線程安全(?thread-safe)的,是指它在任意的多線程場景中都不存在不確定性。線程安全主要是通過鎖以及減少線程交互來實現(xiàn)。 一般的類型很少有完全線程安全的,原因如下:
C#中的多線程?- 同步基礎
1同步概要 在第?1?部分:基礎知識中,我們描述了如何在線程上啟動任務、配置線程以及雙向傳遞數據。同時也說明了局部變量對于線程來說是私有的,以及引用是如何在線程之間共享,允許其通過公共字段進行通信。 下一步是同步(synchronization):為期望的結果協(xié)調線程的行為。當多個線程訪問同一個數據時,同步尤其重要,但是這是一件非常容易搞砸的事情。 同步構造可以分為以下四類:- 簡單的阻塞方法
- 這些方法會使當前線程等待另一個線程結束或是自己等待一段時間。Sleep、Join與Task.Wait都是簡單的阻塞方法。
- 鎖構造
- 鎖構造能夠限制每次可以執(zhí)行某些動作或是執(zhí)行某段代碼的線程數量。排它鎖構造是最常見的,它每次只允許一個線程執(zhí)行,從而可以使得參與競爭的線程在訪問公共數據時不會彼此干擾。標準的排它鎖構造是lock(Monitor.Enter/Monitor.Exit)、Mutex與?SpinLock。非排它鎖構造是Semaphore、SemaphoreSlim以及讀寫鎖。
- 信號構造
- 信號構造可以使一個線程暫停,直到接收到另一個線程的通知,避免了低效的輪詢?。有兩種經常使用的信號設施:事件等待句柄(event?wait?handle?)和Monitor類的Wait?/?Pluse方法。Framework?4.0?加入了CountdownEvent與Barrier類。
- 非阻塞同步構造
- 非阻塞同步構造通過調用處理器指令來保護對公共字段的訪問。CLR?與?C#?提供了下列非阻塞構造:Thread.MemoryBarrier?、Thread.VolatileRead、Thread.VolatileWrite、volatile關鍵字以及Interlocked類。
- 阻塞條件被滿足
- 操作超時(如果指定了超時時間)
- 通過Thread.Interrupt中斷
- 通過Thread.Abort中止
| 構造 | 用途 | 跨進程 | 開銷* |
| lock?(Monitor.Enter/Monitor.Exit) | 確保同一時間只有一個線程可以訪問資源或代碼 | - | 20ns |
| Mutex | ? | 1000ns | |
| SemaphoreSlim?(Framework?4.0?中加入) | 確保只有不超過指定數量的線程可以并發(fā)訪問資源或代碼 | - | 200ns |
| Semaphore | ? | 1000ns | |
| ReaderWriterLockSlim?(Framework?3.5?中加入) | 允許多個讀線程和一個寫線程共存 | - | 40ns |
| ReaderWriterLock?(已過時) | - | 100ns |
- 完全線程安全的開發(fā)負擔很重,特別是如果一個類型有很多字段的情況(在任意多線程并發(fā)的情況下每個字段都有交互的潛在可能)。
- 線程安全可能會損失性能(某種程度上,無論類型是否實際被用于多線程都會增加損耗)。
- 線程安全的類型并不能確保使用該類型的程序也是線程安全的,為了實現(xiàn)程序線程安全所涉及的工作經常會使得類型線程安全成為多余。
- WPF?中:在其Dispatcher對象上調用Invoke或BeginInvoke。
- Windows?Forms?中:調用Control對象上的Invoke或BeginInvoke。
- BackgroundWorker
- 任務延續(xù)(Task?continuations)
轉載于:https://www.cnblogs.com/chinanetwind/articles/9459538.html
總結
以上是生活随笔為你收集整理的C#中的多线程 - 同步基础的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 还呗还信用卡多久到账?没到账怎么办?
- 下一篇: c# char unsigned_dll