我們在編程的時候有時會使用多線程來解決問題比如你的程序需要在後台處理一大堆數據但還要使用戶界面處於可操作狀態或者你的程序需要訪問一些外部資源如數據庫或網絡文件等這些情況你都可以創建一個子線程去處理然而多線程不可避免地會帶來一個問題就是線程同步的問題如果這個問題處理不好我們就會得到一些非預期的結果
在網上也看過一些關於線程同步的文章其實線程同步有好幾種方法下面我就簡單的做一下歸納
一volatile關鍵字
volatile是最簡單的一種同步方法當然簡單是要付出代價的它只能在變量一級做同步volatile的含義就是告訴處理器 不要將我放入工作內存 請直接在主存操作我(「轉自 」)因此當多線程同時訪問該變量時都將直接操作主存從本質上做到了變量共享
能夠被標識為volatile的必須是以下幾種類型
Any reference type
Any pointer type (in an unsafe context)
The types sbyte byte short ushort int uint char float bool
An enum type with an enum base type of byte sbyte short ushort int or uint
如
Code
public class A
{
private volatile int _i;
public int I
{
get { return _i; }
set { _i = value; }
}
}
但volatile並不能實現真正的同步因為它的操作級別只停留在變量級別而不是原子級別如果是在單處理器系統中是沒有任何問題的變量在主存中沒有機會被其他人修改因為只有一個處理器這就叫作processor SelfConsistency但在多處理器系統中可能就會有問題 每個處理器都有自己的data cach而且被更新的數據也不一定會立即寫回到主存所以可能會造成不同步但這種情況很難發生因為cach的讀寫速度相當高flush的頻率也相當高只有在壓力測試的時候才有可能發生幾率非常非常小
二lock關鍵字
lock是一種比較好用的簡單的線程同步方式它是通過為給定對象獲取互斥鎖來實現同步的它可以保證當一個線程在關鍵代碼段的時候另一個線程不會進來它只能等待等到那個線程對象被釋放也就是說線程出了臨界區用法
Code
public void Function()
{
object lockThis = new object ();
lock (lockThis)
{
// Access threadsensitive resources
}
}
lock的參數必須是基於引用類型的對象不要是基本類型像boolint什麼的這樣根本不能同步原因是lock的參數要求是對象如果傳入int勢必要發生裝箱操作這樣每次lock的都將是一個新的不同的對象最好避免使用public類型或不受程序控制的對象實例因為這樣很可能導致死鎖特別是不要使用字符串作為lock的參數因為字符串被CLR暫留就是說整個應用程序中給定的字符串都只有一個實例因此更容易造成死鎖現象建議使用不被暫留的私有或受保護成員作為參數其實某些類已經提供了專門用於被鎖的成員比如Array類型提供SyncRoot許多其它集合類型也都提供了SyncRoot
所以使用lock應該注意以下幾點
1如果一個類的實例是public的最好不要lock(this)因為使用你的類的人也許不知道你用了lock如果他new了一個實例並且對這個實例上鎖就很容易造成死鎖
2如果MyType是public的不要lock(typeof(MyType))
3永遠也不要lock一個字符串
三SystemThreadingInterlocked
對於整數數據類型的簡單操作可以用 Interlocked 類的成員來實現線程同步存在於SystemThreading命名空間Interlocked類有以下方法Increment Decrement Exchange 和CompareExchange 使用Increment 和Decrement 可以保證對一個整數的加減為一個原子操作Exchange 方法自動交換指定變量的值CompareExchange 方法組合了兩個操作比較兩個值以及根據比較的結果將第三個值存儲在其中一個變量中比較和交換操作也是按原子操作執行的如
Code
int i = ;
SystemThreadingInterlockedIncrement( ref i);
ConsoleWriteLine(i);
SystemThreadingInterlockedDecrement( ref i);
ConsoleWriteLine(i);
SystemThreadingInterlockedExchange( ref i );
ConsoleWriteLine(i);
SystemThreadingInterlockedCompareExchange( ref i );
Output:
四Monitor
Monitor類提供了與lock類似的功能不過與lock不同的是它能更好的控制同步塊當調用了Monitor的Enter(Object o)方法時會獲取o的獨占權直到調用Exit(Object o)方法時才會釋放對o的獨占權可以多次調用Enter(Object o)方法只需要調用同樣次數的Exit(Object o)方法即可Monitor類同時提供了TryEnter(Object o[int])的一個重載方法該方法嘗試獲取o對象的獨占權當獲取獨占權失敗時將返回false
但使用 lock 通常比直接使用 Monitor 更可取一方面是因為 lock 更簡潔另一方面是因為 lock 確保了即使受保護的代碼引發異常也可以釋放基礎監視器這是通過 finally 中調用Exit來實現的事實上lock 就是用 Monitor 類來實現的下面兩段代碼是等效的
Code
lock (x)
{
DoSomething();
}
等效於
object obj = ( object )x;
SystemThreadingMonitorEnter(obj);
try
{
DoSomething();
}
finally
{
SystemThreadingMonitorExit(obj);
}
關於用法請參考下面的代碼
Code
private static object m_monitorObject = new object ();
[STAThread]
static void Main( string [] args)
{
Thread thread = new Thread( new ThreadStart(Do));
threadName = Thread ;
Thread thread = new Thread( new ThreadStart(Do));
threadName = Thread ;
threadStart();
threadStart();
threadJoin();
threadJoin();
ConsoleRead();
}
static void Do()
{
if ( ! MonitorTryEnter(m_monitorObject))
{
ConsoleWriteLine( Cant visit Object + ThreadCurrentThreadName);
return ;
}
try
{
MonitorEnter(m_monitorObject);
ConsoleWriteLine( Enter Monitor + ThreadCurrentThreadName);
ThreadSleep( );
}
finally
{
MonitorExit(m_monitorObject);
}
}
當線程獲取了m_monitorObject對象獨占權時線程嘗試調用TryEnter(m_monitorObject)此時會由於無法獲取獨占權而返回false輸出信息如下
另外Monitor還提供了三個靜態方法MonitorPulse(Object o)MonitorPulseAll(Object o)和MonitorWait(Object o ) 用來實現一種喚醒機制的同步關於這三個方法的用法可以參考MSDN這裡就不詳述了
五Mutex
在使用上Mutex與上述的Monitor比較接近不過Mutex不具備WaitPulsePulseAll的功能因此我們不能使用Mutex實現類似的喚醒的功能不過Mutex有一個比較大的特點Mutex是跨進程的因此我們可以在同一台機器甚至遠程的機器上的多個進程上使用同一個互斥體盡管Mutex也可以實現進程內的線程同步而且功能也更強大但這種情況下還是推薦使用Monitor因為Mutex類是win封裝的所以它所需要的互操作轉換更耗資源
六ReaderWriterLock
在考慮資源訪問的時候慣性上我們會對資源實施lock機制但是在某些情況下我們僅僅需要讀取資源的數據而不是修改資源的數據在這種情況下獲取資源的獨占權無疑會影響運行效率因此Net提供了一種機制使用ReaderWriterLock進行資源訪問時如果在某一時刻資源並沒有獲取寫的獨占權那麼可以獲得多個讀的訪問權單個寫入的獨占權如果某一時刻已經獲取了寫入的獨占權那麼其它讀取的訪問權必須進行等待參考以下代碼
Code
private static ReaderWriterLock m_readerWriterLock = new ReaderWriterLock();
private static int m_int = ;
[STAThread]
static void Main(string[] args)
{
Thread readThread = new Thread(new ThreadStart(Read));
readThreadName = ReadThread;
Thread readThread = new Thread(new ThreadStart(Read));
readThreadName = ReadThread;
Thread writeThread = new Thread(new ThreadStart(Writer));
writeThreadName = WriterThread;
readThreadStart();
readThreadStart();
writeThreadStart();
readThreadJoin();
readThreadJoin();
writeThreadJoin();
ConsoleReadLine();
}
private static void Read()
{
while (true)
{
ConsoleWriteLine(ThreadName + ThreadCurrentThreadName + AcquireReaderLock);
m_readerWriterLockAcquireReaderLock();
ConsoleWriteLine(StringFormat(ThreadName : {} m_int : {} ThreadCurrentThreadName m_int));
m_readerWriterLockReleaseReaderLock();
}
}
private static void Writer()
{
while (true)
{
ConsoleWriteLine(ThreadName + ThreadCurrentThreadName + AcquireWriterLock);
m_readerWriterLockAcquireWriterLock();
InterlockedIncrement(ref m_int);
ThreadSleep();
m_readerWriterLockReleaseWriterLock();
ConsoleWriteLine(ThreadName + ThreadCurrentThreadName + ReleaseWriterLock);
}
}
在程序中我們啟動兩個線程獲取m_int的讀取訪問權使用一個線程獲取m_int的寫入獨占權執行代碼後輸出如下
可以看到當WriterThread獲取到寫入獨占權後任何其它讀取的線程都必須等待直到WriterThread釋放掉寫入獨占權後才能獲取到數據的訪問權應該注意的是上述打印信息很明顯顯示出可以多個線程同時獲取數據的讀取權這從ReadThread和ReadThread的信息交互輸出可以看出
七SynchronizationAttribute
當我們確定某個類的實例在同一時刻只能被一個線程訪問時我們可以直接將類標識成Synchronization的這樣CLR會自動對這個類實施同步機制實際上這裡面涉及到同步域的概念當類按如下設計時我們可以確保類的實例無法被多個線程同時訪問
) 在類的聲明中添加SystemRuntimeRemotingContextsSynchronizationAttribute屬性
) 繼承至SystemContextBoundObject
需要注意的是要實現上述機制類必須繼承至SystemContextBoundObject換句話說類必須是上下文綁定的
一個示范類代碼如下
Code
[SystemRuntimeRemotingContextsSynchronization]
public class SynchronizedClass : SystemContextBoundObject
{
}
八MethodImplAttribute
如果臨界區是跨越整個方法的也就是說整個方法內部的代碼都需要上鎖的話使用MethodImplAttribute屬性會更簡單一些這樣就不用在方法內部加鎖了只需要在方法上面加上 [MethodImpl(MethodImplOptionsSynchronized)] 就可以了MehthodImpl和MethodImplOptions都在命名空間SystemRuntimeCompilerServices 裡面但要注意這個屬性會使整個方法加鎖直到方法返回才釋放鎖因此使用上不太靈活如果要提前釋放鎖則應該使用Monitor或lock我們來看一個例子
Code
[MethodImpl(MethodImplOptionsSynchronized)]
public void DoSomeWorkSync()
{
ConsoleWriteLine( DoSomeWorkSync() Lock held by Thread +
ThreadCurrentThreadGetHashCode());
ThreadSleep( );
ConsoleWriteLine( DoSomeWorkSync() Lock released by Thread +
ThreadCurrentThreadGetHashCode());
}
public void DoSomeWorkNoSync()
{
ConsoleWriteLine( DoSomeWorkNoSync() Entered Thread is +
ThreadCurrentThreadGetHashCode());
ThreadSleep( );
ConsoleWriteLine( DoSomeWorkNoSync() Leaving Thread is +
ThreadCurrentThreadGetHashCode());
}
[STAThread]
static void Main( string [] args)
{
MethodImplAttr testObj = new MethodImplAttr();
Thread t = new Thread( new ThreadStart(testObjDoSomeWorkNoSync));
Thread t = new Thread( new ThreadStart(testObjDoSomeWorkNoSync));
tStart();
tStart();
Thread t = new Thread( new ThreadStart(testObjDoSomeWorkSync));
Thread t = new Thread( new ThreadStart(testObjDoSomeWorkSync));
tStart();
tStart();
ConsoleReadLine();
}
這裡我們有兩個方法我們可以對比一下一個是加了屬性MethodImpl的DoSomeWorkSync()一個是沒加的DoSomeWorkNoSync()在方法中Sleep()是為了在第一個線程還在方法中時第二個線程能夠有足夠的時間進來對每個方法分別起了兩個線程我們先來看一下結果
可以看出對於線程1和2也就是調用沒有加屬性的方法的線程當線程2進入方法後還沒有離開線程1有進來了這就是說方法沒有同步我們再來看看線程3和4當線程3進來後方法被鎖直到線程3釋放了鎖以後線程4才進來
九同步事件和等待句柄
用lock和Monitor可以很好地起到線程同步的作用但它們無法實現線程之間傳遞事件如果要實現線程同步的同時線程之間還要有交互就要用到同步事件同步事件是有兩個狀態(終止和非終止)的對象它可以用來激活和掛起線程
同步事件有兩種AutoResetEvent和 ManualResetEvent它們之間唯一不同的地方就是在激活線程之後狀態是否自動由終止變為非終止AutoResetEvent自動變為非終止就是說一個AutoResetEvent只能激活一個線程而ManualResetEvent要等到它的Reset方法被調用狀態才變為非終止在這之前ManualResetEvent可以激活任意多個線程
可以調用WaitOneWaitAny或WaitAll來使線程等待事件它們之間的區別可以查看MSDN當調用事件的 Set方法時事件將變為終止狀態等待的線程被喚醒
來看一個例子這個例子是MSDN上的因為事件只用於一個線程的激活所以使用 AutoResetEvent 或 ManualResetEvent 類都可以
Code
static AutoResetEvent autoEvent;
static void DoWork()
{
ConsoleWriteLine( worker thread started now waiting on event);
autoEventWaitOne();
ConsoleWriteLine( worker thread reactivated now exiting);
}
[STAThread]
static void Main(string[] args)
{
autoEvent = new AutoResetEvent(false);
ConsoleWriteLine(main thread starting worker thread);
Thread t = new Thread(new ThreadStart(DoWork));
tStart();
ConsoleWriteLine(main thrad sleeping for second);
ThreadSleep();
ConsoleWriteLine(main thread signaling worker thread);
autoEventSet();
ConsoleReadLine();
}
我們先來看一下輸出
在主函數中首先創建一個AutoResetEvent的實例參數false表示初始狀態為非終止如果是true的話初始狀態則為終止然後創建並啟動一個子線程在子線程中通過調用AutoResetEvent的WaitOne方法使子線程等待指定事件的發生然後主線程等待一秒後調用AutoResetEvent的Set方法使狀態由非終止變為終止重新激活子線程
From:http://tw.wingwit.com/Article/program/net/201311/12129.html