同步上下文;
同步代碼區;
手控同步.
1 同步上下文
上下文是一組屬性或使用規則,這組屬性或使用規則對執行時相關的對象集合是通用的.能夠添加的上下文屬性包括有關同步,線程式親緣性的事務處理的策略.簡言之,上下文把類似的對象組合在一起.這種策略將使用SynchronizationArrribute類對ContextBoundObject對象進行簡單的自動同步,駐留在上下文中,符合上下文規則的對象稱爲上下文對象稱爲上下文綁定對象. .NET把同步鎖和對象自動關聯起來.在每種方法調用前鎖定對象,方法返回後釋放對象(讓其它等待着的線程訪問).由於線程同步和併發管理是開發人員遇到的最困難的任務.因此這種方法極大的提高了效率.
SynochronizationAttribute 類對缺少手工處理同步經難的程序員來說是很有用的.因爲它包含了實例變量,實例方法和應用這個屬性的類的實例字段.然而,它不是處理靜態字段和方法同步.如果必須同步代碼塊,它也不起作用.同步整個對象是對輕鬆使用必須付出的代碼.在使用SynchronizationAttribute編程時,SynchronizationAttribute非常方便.因爲一個上下文(例如事務處理)中的對象由 COM+ 運行庫組合在一起.我們可以使用下面的代碼來使用一個類具有安全性:
[SynchronizationAttribute(SynchronizationOption.Required)]
public class className
{
//class code here
}
SynchronizationAttribute類有兩個構造函數.一個不帶參數(默認SynchronizationOption.Required),另一個構造函數包含一個SynchronizationOption 枚舉.這個枚舉的值如下所示:
Disabled :忽略對象的同步請求,即對象不是線程安全的.
NotSupported :創建組件時沒有控制同步,即論調用者牌什麼狀態,對象都不能參與任何同步操作.
Required :確保所創建的所有的對象都已同步.
RequiredNew :無論調用者是什麼,組件總是參與新的同步.
Supported :使用這個選項的對象只有存在(取決於調用者)時,才能參與同步操作.
2 同步代碼區
第二種同步策略是同步代碼區,這些特定的代碼區是方法中重要的代碼段.它們可以改變對象的狀態,或者更新另一個資源(如數據庫,文件等).先看Monitor類.
Monitor類用於同步代碼區,其方式是使用Monitor.Enter()方法獲得一個鎖.然後,使用Monitor.Exit()方法釋放該鎖.鎖的概念通常用於解釋Monitor類.一個線程獲得鎖,其它線程就要等到該鎖釋放後才能使用.一旦在代碼區上獲取了一個鎖,就可以在Monitor.Enter()和Monitor.Exit()程序塊內使用如下方法:
Wait()--此方法用於釋放對象上的鎖.並暫停當前的線程.直到它重新獲得鎖.
Pulse()-此方法用於通知正在隊列中等待的線程,對象的狀態已經改變了.
PulseAll():此方法用於通知所有正在隊列中等待的線程,對象的狀態已經有了改變.
Enter()方法和Exit()方法
Monitor方法是靜態方法.能夠被Monitor類自身調用,而不是由該類的實例調用.在.NET Framework中,每個對象都有一個與之相關的鎖,可以獲取和釋放該鎖,這樣,在任一時刻僅有一個線程可以訪問對象的實例變量和方法.與之類似,.NET Framework中的每個對象也提供一種允許它處於等待狀態的機制.正如鎖的機制. 設計這種機制的主要原因是幫助線程間的通信.如果一個線程進入對象的重要代碼段,並需要一定的條件才能存在,而另一個線程可以在該代碼段中創建該條件,此時就需要這種機制.
訣竅在於,在任一時刻,重要的代碼段只允許一個線程訪問,當第一個線程進入代碼段後,其它的線程就不能再進入了,如果第線程A從數據庫中獲取了一些數據,在線程A接收到所有的數據,並處理這些數據的過程中,另一個線程B必須調用Wait()方法.等待線程A通知它何時才能接收數據.在接收到數據時,線程A就調用Pulse()方法,通知線程B,讓線程B處理數據.這可以通過"等待和發出脈衝"機制來實現.第一個線程進入重要代碼段,執行Wait() 方法. Wait() 方法先釋放鎖, 再使第一個線程進入等待狀態.現在第二個線程進入代碼段.修改必須的條件.然後調用Pulse() 方法來通知等待的線程.當條件滿足後,就可以繼續執行.接着第一個線程先重新獲得鎖,再從Monitor.Wait() 方法中返回,並從調用Monitor.Wait()的地方繼續執行.任何兩個線程都不能同時進入 Enter() 方法.
下面是一個有關Monitor的例子:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
namespace ConsoleApplication1
{
class Program
{
private int result = 0;
public Program()
{
}
public void NonCriticalSection()
{
Console.WriteLine("Enter thread " + Thread.CurrentThread.GetHashCode());
for (int i = 0; i <= 15; i++)
{
Console.WriteLine("Result = " + result++ + " ThreadID=" + Thread.CurrentThread.GetHashCode());
Thread.Sleep(2500);
}
Console.WriteLine("Exiting Thread" + Thread.CurrentThread.GetHashCode());
}
public void CriticalSection()
{
Monitor.Enter(this);
Console.WriteLine("Entered Thread " + Thread.CurrentThread.GetHashCode());
for (int i = 0; i <= 15; i++)
{
Console.WriteLine("Result = " + result++ + " ThreadID=" + Thread.CurrentThread.GetHashCode());
Thread.Sleep(1500);
}
Console.WriteLine("Exiting Thread" + Thread.CurrentThread.GetHashCode());
Monitor.Exit(this);
}
public static void Main()
{
Program p = new Program();
int i = Console.Read();
if (i % 2 == 0)
{
Thread t1 = new Thread(new ThreadStart(p.NonCriticalSection));
t1.Start();
Thread t2 = new Thread(new ThreadStart(p.NonCriticalSection));
t2.Start();
}
else
{
Thread t1 = new Thread(new ThreadStart(p.CriticalSection));
t1.Start();
Thread t2 = new Thread(new ThreadStart(p.CriticalSection));
t2.Start();
}
}
}
}
在上面的例子中,重要代碼段定義爲 Monitor.Enter(this) 和 Monitor.Exit(this)之間的代碼塊.參數this指定應將鎖保存在當前對象上,把哪個對象作爲參數傳遞給Enter()方法況是不好確定.在需要鎖定對象,不上其他線程訪問它時,應把指針this作爲參數傳遞.
Wait()和Pulse()機制
Wait()和Pulse()機制用於線程式間的交互.當在一個對象上執行Wait()時,正在訪問該對象的線程就會進入等待狀態,直到它得到一個喚醒信號.Pulse()和PulseAll()用於給等待線程發送信號.下面列出的上一個Wait()和Pulse()方法如何工作的例子:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
namespace ConsoleApplication1
{
class LockMe
{
public LockMe()
{
}
}
class Monitor1
{
public LockMe _lm;
public Monitor1(LockMe lm)
{
this._lm = lm;
}
public void CriticalSection()
{
Monitor.Enter(this._lm);
Console.WriteLine("Enter M1");
for(int i= 0 ;i < 10; i++)
{
Thread.Sleep(3000);
Console.WriteLine("M1 Wait");
Monitor.Wait(this._lm);
Console.WriteLine("M1 WorkUP");
Monitor.Pulse(this._lm);
}
Monitor.Exit(this._lm);
}
}
class Monitor2
{
private LockMe _lm;
public Monitor2(LockMe lm)
{
this._lm = lm;
}
public void CriticalSection()
{
Monitor.Enter(this._lm);
Console.WriteLine("Enter M2");
for (int i = 0; i < 10; i++)
{
Thread.Sleep(3000);
Console.WriteLine("M2 WorkUp");
Monitor.Pulse(this._lm);
Console.WriteLine("M2 Wait");
Monitor.Wait(this._lm);
}
Monitor.Exit(this._lm);
}
}
class Program
{
public static void Main()
{
LockMe l = new LockMe();
Monitor1 m1 = new Monitor1(l);
Monitor2 m2 = new Monitor2(l);
Thread t1 = new Thread(new ThreadStart(m1.CriticalSection));
t1.Start();
Thread t2 = new Thread(new ThreadStart(m2.CriticalSection));
t2.Start();
}
}
}
注意:當線程執行Monitor.Wait()方法時,它會臨時釋放LockMe對象上的鎖,以便其它線程可以訪問它.線程 t1 進入等待狀態後, 線程t2就可以自由訪問 LockMe 對象.即使LockMe對象是一個獨立的對象,這兩個線程也均指向同一個對象的引用. 線程t2獲得LockMe對象上的鎖後,一進入For循環,就給LockMe對象上等待的線程發送一個運行進通知.之後便進行等待狀態.然後t1 醒來,獲得LockMe對象上的鎖,接着 t1 執行一些操作,並給 LockMe對象上等待的線程發送一個運行時通知.這個循環一直持續到For循環結束.
注意每一個 Enter() 方法都要伴隨一個 Exit()方法,否則程序就會進入列循環.
Enter將一個對象作爲其參數,如果該對象參數爲 null, 方法變量或者值類型對象如整型值,系統將會拋出一個異常.
TryEnter()方法
Moniter類的TryEnter()方法非常類似於Enter()方法,它試圖獲得對象的獨佔鎖,不過它不會像Enter()方法那樣暫停.如果線程成功進入,則TryEnter()方法返回True.
TryEnter()方法有三個重載,其中兩個都帶超時參數,表示等待鎖定的時間.如:
Monitor.TryEnter(this,1000);
......
在可能發生競爭,但不希望線程睡眠某個示指定的時間,就可以使用TryEnter().
Lock
Lock關鍵字可以用作monitor方法的一個替換用法,下面的兩部分代碼是等價的:
Monitor.Enter(X);
.....
Monitor.Exit(X);
lock(this)
{
.....
}
ReaderWriterLock類
ReaderWriterLock定義了實現單寫程序和多讀程序語義的鎖.這個類主要用於文件操作.即多個線程可以讀取文件,但只能用一個線程來更新文件.ReaderWriterLock類中4個主要的方法是:
AcquireReaderLock():該重載方法獲得一個讀程序鎖,超時值使用整數或TimeSpan.超時是可用於檢測死鎖的好工具.
AcquireWriterLock():該重載方法獲得一個寫程序鎖.超時值使用整數或TimeSpan.
ReleaseReaderLock():釋放讀程序鎖.
ReleaseWriterLock():釋放寫程序鎖.
使用ReaderWriterLock類時,任意數量的線程都可以同時安全的讀取數據.只有當線程進行更新時,數據才被鎖定.只有在沒有佔用鎖的寫程序線程時,讀程序線程才能獲得鎖.只有沒有佔用鎖的讀程序或寫程序線程時,寫程序才能獲得鎖.
3手控同步
DotNet Framework提供了一套經典的技術允許程序員使用類似於Win32線程的低級線程API創建和管理多線程應用程序.下表列出了System.Threading命名空間中的一些可以用於手控同步和類.
AutoResetEvent:
AutoResetEvent類可以使線程處於等待狀態.直到通過調用Set()方法某事件將它置於信號狀態爲止.有信號狀態是指沒有線程在等待.在釋放一個等待線程後.系統自動將AutoResetEvent類生置爲無信號狀態.如果沒正在等待的線程,事件對象的狀態保持警惕爲有信號狀態.指定ManualRest參數爲False時,AutoResetEvent類對應於Win32的CreateEvent調用.
ManualResetEvent
ManualResetEvent類也用來使線程式處於等待狀態.直到通過調用Set()方法某事件將它置於有信號狀態.ManualResetEvent對象的狀態會一直保持有信號,直到Reset()方法將它設置爲無信號狀態.指定 bManualReset 參數爲True時, ManualResetEvent對應於Win32的CreateEvent調用.
Mutex
Mutex鎖提供了跨進程和跨線程同步.如果沒有線程佔用Mutex鎖,Mutex的狀態就軒爲有信號狀態.Mutex並不上具有Monitor類的所有等待和發出脈衝功能.但可以創建在進程之間使用的命名互斥(使用重載構造函數),使用Mutex 類優於使用 Monitor 類,因爲Mutex類可以跨進程使用,而Monitor類不行.
Interlocked
Interlocked類爲在多個線程之間共享的原子的,非暫停整體更新提供了方法.如果變量位於共享的內在中,不同進程的線程就可以使用這種機制.
ManualResetEvent類
ManulaResetEvent類對象只能擁有兩種狀態之一:有信號(True)和無信號(False),ManualResetEvent類繼承於WaitHandler類,其構造函數的參數可以確定對象的初始狀態,Set()和Reset()方法返回一個布爾值,表示是否進行了成功的修改.
下面的代碼首先創建一個對象 mansig , 並賦予false值,方法WaitOne()將一直等待 mansig 變爲 true 或超時爲止. 由於時間在等待中過去.且 mansig 的值沒有被設置障爲 true,所以程序就停止暫停並返回 false值.
public static void Main(string [] args)
{
ManualResetEvent mansig;
mansig = new ManualResetEvent(false);
Console.WriteLine("ManualResetEvent Before WaitOne");
bool b = mansig.WaitOne(1000,false);
Console.WriteLine("ManualResetEvent After WaitOne" + b);
}
上面的代碼輸出如下:
ManualResetEvent Before Waitone
ManulaResetEvent After WaitOne false
布爾值 false 把ManualResetEvent對象的初始狀態設置爲無信號,接着調用基類的 WaitHandle的 WaitOne() 方法,WaitOne()方法有兩個參數,第一個參數是線程在WaitOne()方法中等待的毫秒數,因此在線程退出前等待一秒,第二個參數是exitContext , 如果已以在上下文的同步域中,想退出同步上下文,或者要重新獲得同步上下文,就把該參數設置爲 true;程序在WaitOne()方法中暫停一秒,然後因爲超時而退出.ManulaResetEvent的狀態仍然是 false,因而WaitOne()返回的布爾值是 false. 如果在創建 ManualResetEvent時就把它的狀態設定爲有信號(treu)時,如下代碼:
ManualResetEvent mansig;
mansig = new ManualResetEvent(true):
Console.WriteLine("ManualResetEvent Before WaitOne");
bool b = mansig.WaitOne(1000,false);
Console.WriteLine("ManualResetEvent After WaitOne " + b);
輸出結果如下所示:
ManualResetEvent Before WaitOne
ManualResetEvent After WaitOne true
將ManualResetEvent的初始狀態修改爲有信號的,即使指定超時值爲1000毫秒,線程在WaitOne()中也不會等待.當ManualResetEvent的狀態爲無信號狀態時,線程將等待狀態變爲有信號狀態. 但1000毫秒後線程超時.狀態 已經是有信號的.必須調用ManualResetEvent 的Reset()方法.爲了把狀態修改爲有信號的,必須調用Set()方法.
下面的代碼演示瞭如何使用 Set() 方法和 Reset()方法.
static void Main()
{
ManualResetEvent mre = new ManualResetEvent(true);
mre = new ManualResetEvent(1000,true);
bool state = mre.WaitOne(1000,true);
Console.WriteLine("ManualResetEvent After WaitOne" + state);
//change the state non-signaled
mre.Reset();
state = mre.WaitOne(5000,true);
Console.WriteLine("ManualResetEvent After secodn WaitOne" + state);
}
程序輸出:
ManualResetEvent Ater first WaitOne true
ManualResetEvent After Second WaitOne false
ManualResetEvent對象的構造函數在將其初始化時設置爲有信號的(true),結果,線程不在第一個WaitOne()方法中等待,並返回 true 值,接着將ManualResetEvent對象的狀態重新設置爲無信號的(false),於是線程在超時之間等待5秒鐘.
ManualResetEvent對象的WaitAll() 方法將等待所有的事件對象都變成 true 或者有信號時,否則它將保持不變直到超時.WaitAny()方法等待任一事件對象變成true或有信號.
AutoResetEvent類
AutoResetEvent類的工作方式類似於ManualResetEvent類,它等待時間超時或者事件變成有信號的狀態.接着將此事件通知等待線程.ManualResetEvent和AutoResetEvent的一個重要區別就是AutoResetEvent在WaitOne()方法中改變狀態.下面的代碼展示了AutoResetEvent類的用法:
static void Main()
{
AutoResetEvent are = new AutoResetEvent (true):
Console.WriteLine("Before First WaitOne");
bool state = are.WaitOne(1000,true);
Console.WriteLine("After First WaitOne " + state);
state = are.WaitOne(5000,true);
Console.WriteLine("After Second WaitOne" + state);
}
靜態變量,方法和同步
靜態變量和方法與同步鎖中的實例 變量和方法有不同的作用.靜態變量是類變量,而屬於一個對象的變量是對象或者實例變量.靜態變量只能有一個實例,靜態方法由同一類的多個對象共享.同一類的每個對象都有自己的一組實例變量和方法.如果同步一個靜態變量或者靜態方法,鎖就要應用於整個類上,其結果是,其它對象不能使用此類的靜態變量.
ThreadStaticAttribute 類
ThreadStaticAttribute類用於在靜態變量上爲每個執行它的線程創建一個單獨的變量,而不是在線程之間共享(默認行爲)靜態變量.這意味着,帶有ThreadStaticAttribute的靜態變量不會在訪問變量的不同線程之間共享.每個訪問變量的線程都有該變量的一個副本.如果一個線程修改了變量,另一個訪問變量的線程就不能看到這些變化.這種行爲是有背於靜態變量的默認行爲的.總之,ThreadStaticAttribute提供了最好的方法(靜態和實例).如下代碼:
{
static void Main(string[] args)
{
StaticThread ts = new StaticThread();
Thread t1 = new Thread(new ThreadStart(ts.Run));
Thread t2 = new Thread(new ThreadStart(ts.Run));
t1.Start();
t2.Start();
Thread.Sleep(1000000);
}
}
class StaticThread
{
[System.ThreadStatic]
public static int x = 1;
public static int y = 1;
public void Run()
{
for (int i = 0; i <= 10; i++)
{
Thread t2 = Thread.CurrentThread;
x++;
y++; Console.WriteLine("i=" + i + "ThreadID=" + t2.GetHashCode() + "x(static attribute)= " + x + " y = " + y);
Thread.Sleep(1000);
}
}
}
通過運行上面的代碼可知:靜態變量是類量,它的值在類的多個對象中保持不變,ThreadStaticAttribute允許每個訪問靜態變量的線程擁有自己的副本,上面的代碼中,變量x 使 ThreadStaticAttribute應用於它,結果,線程t1和t2將分別擁有靜態變量x的一個副本,並且線程t1對變量x的修改對線程t2來說是不可見的.另一方面,線程t1對變量y的修改對線程t2來說是可見的.
帶有ThreadStaticAttribute的靜態變量和實例變量之間的區別是:靜態變量不需求對象訪問它,但如果沒有創建對象的實例就試圖訪問實例變量,剛會拋出異常.