平常在多線程開發中,總避免不了線程同步。本篇對net多線程中的鎖系統做個簡單描述。
閱讀目錄:
- lock、Monitor
- 作用域範圍
- 字元串鎖
- Monitor的用法
- Mutex
- Semaphore
- 總結
lock、Monitor
Lock是Monitor文法糖簡化寫法,Lock在IL會生成Monitor。
//======Example 1=====
string obj = "helloworld";
lock (obj)
{
Console.WriteLine(obj);
}
//lock IL會編譯成如下寫法
bool isGetLock = false;
Monitor.Enter(obj, ref isGetLock);
try
{
Console.WriteLine(obj);
}
finally
{
if (isGetLock)
{
Monitor.Exit(obj);
}
}
複制
isGetLock參數是Framework 4.0後新加的。 為了使程式在所有情況下都能夠确定,是否有必要釋放鎖。例: Monitor.Enter拿不到鎖
Monitor.Enter 是可以鎖值類型的。鎖時會裝箱成新對象,是以無法做到線程同步。
作用域範圍
一:Lock是隻能在程序内鎖,不能跨程序,内部走的是混合構造,先自旋再轉成核心構造。
二:關于對type類型的鎖,如下:
//======Example 2=====
new Thread(new ThreadStart(() => {
lock (typeof(int))
{
Thread.Sleep(10000);
Console.WriteLine("Thread1釋放");
}
})).Start();
Thread.Sleep(1000);
lock(typeof(int))
{
Console.WriteLine("Thread2釋放");
}
複制
運作結果如下:

在看個例子:
//======Example 3=====
Console.WriteLine(DateTime.Now);
AppDomain appDomain1 = AppDomain.CreateDomain("AppDomain1");
LockTest Worker1 = (LockTest)appDomain1.CreateInstanceAndUnwrap(
Assembly.GetExecutingAssembly().FullName,
"ConsoleApplication1.LockTest");
Worker1.Run();
AppDomain appDomain2 = AppDomain.CreateDomain("AppDomain2");
LockTest Worker2 = (LockTest)appDomain2.CreateInstanceAndUnwrap(
Assembly.GetExecutingAssembly().FullName,
"ConsoleApplication1.LockTest");
Worker2.Run();
/// <summary>
/// 跨應用程式域邊界或遠端通路時需要繼承MarshalByRefObject
/// </summary>
public class LockTest : MarshalByRefObject
{
public void Run()
{
lock (typeof(int))
{
Thread.Sleep(10000);
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + ": Thread 釋放," + DateTime.Now);
}
}
}
複制
運作結果如下:
第一個例子說明,在同程序同域,不同線程下,鎖type int,其實鎖的是同一個int對象,是以要慎用。
第二個例子,這裡就簡單說下。
A: CLR啟動時,會建立 系統域(System Domain)和共享域(Shared Domain), 預設程式域(Default AppDomain)。 系統域和共享域是單例的。程式域可以有多個,例子中我們使用AppDomain.CreateDomain方法建立的。
B: 按正常來說,每個程式域的代碼都是隔離,互不影響的。但對于一些基礎類型來說,每個程式域都重新加載一份,就顯得有點浪費,帶來額外的損耗壓力。聰明的CLR會把一些基本類型Object, ValueType, Array, Enum, String, and Delegate等所在的程式集MSCorLib.dll,在CLR啟動過程中都會加載到共享域。 每個程式域都會使用共享域的基礎類型執行個體。
C: 而每個程式域都有屬于自己的托管堆。托管堆中最重要的是GC heap和Loader heap。GC heap用于引用類型執行個體的存儲,生命周期管理和垃圾回收。Loader heap儲存類型系統,如MethodTable,資料結構等,Loader heap生命周期不受GC管理,跟程式域解除安裝有關。
是以共享域中Loader heap MSCorLib.dll中的int執行個體會一直保留着,直到程序結束。單個程式域解除安裝也不受影響。作用域很大有沒有!!!
這時第二個例子也很容易了解了。 鎖int執行個體是跨程式域的,MSCorLib中的基礎類型都是這樣, 極容易造成死鎖。 而自定義類型則會加載到自己的程式域,不會影響其他。
字元串的鎖
我們都知道鎖的目的,是為了多線程下值被破壞。也知道string在c#是個特殊對象,值是不變的,每次變動都是一個新對象值,這也是推薦stringbuilder原因。如例:
//======Example 4=====
string str1 = "mushroom";
string str2 = "mushroom";
var result1 = object.ReferenceEquals(str1, str2);
var result2 = object.ReferenceEquals(str1, "mushroom");
Console.WriteLine(result1 + "-" + result2);
/* output
* True-True
*/
複制
正是由于c#中字元串的這種特性,是以字元串是在多線程下是不會被修改的,隻讀的。它存在于SystemDomain域中managed heap中的一個hash table中。其中Key為string本身,Value為string對象的位址。
當程式域需要一個string的時候,CLR首先在這個Hashtable根據這個string的hash code試着找對應的Item。如果成功找到,則直接把對應的引用傳回,否則就在SystemDomain對應的managed heap中建立該 string,并加入到hash table中,并把引用傳回。是以說字元串的生命周期是基于整個程序的,也是跨AppDomain。
Monitor的用法
簡單介紹下Wait,Pulse,PulseAll的用法,已加注釋。
static string str = "mushroom";
static void Main(string[] args)
{
new Thread(() =>
{
bool isGetLock = false;
Monitor.Enter(str, ref isGetLock);
try
{
Console.WriteLine("Thread1第一次擷取鎖");
Thread.Sleep(5000);
Console.WriteLine("Thread1暫時釋放鎖,并等待其他線程釋放通知信号。");
Monitor.Wait(str);
Console.WriteLine("Thread1接到通知,第二次擷取鎖。");
Thread.Sleep(1000);
}
finally
{
if (isGetLock)
{
Monitor.Exit(str);
Console.WriteLine("Thread1釋放鎖");
}
}
}).Start();
Thread.Sleep(1000);
new Thread(() =>
{
bool isGetLock = false;
Monitor.Enter(str, ref isGetLock); //一直等待中,直到其他釋放。
try
{
Console.WriteLine("Thread2獲得鎖");
Thread.Sleep(5000);
Monitor.Pulse(str); //通知隊列裡一個線程,改變鎖狀态。 Pulseall 通知所有的
Console.WriteLine("Thread2通知其他線程,改變狀态。");
Thread.Sleep(1000);
}
finally
{
if (isGetLock)
{
Monitor.Exit(str);
Console.WriteLine("Thread2釋放鎖");
}
}
}).Start();
Console.ReadLine();
複制
Mutex
lock是不能跨程序鎖的。 mutex作用和lock類似,但是它能跨程序鎖資源(走的是windows核心構造),如例子:
static bool createNew = false;
//第一個參數 是否應擁有互斥體的初始所屬權。即createNew true時,mutex預設獲得處理信号
//第二個是名字,第三個是否成功。
public static Mutex mutex = new Mutex(true, "mushroom.mutex", out createNew);
static void Main(string[] args)
{
//======Example 5=====
if (createNew) //第一個建立成功,這時候已經拿到鎖了。 無需再WaitOne了。一定要注意。
{
try
{
Run();
}
finally
{
mutex.ReleaseMutex(); //釋放目前鎖。
}
}
//WaitOne 函數作用是阻止目前線程,直到拿到收到其他執行個體釋放的處理信号。
//第一個參數是等待逾時時間,第二個是否退出上下文同步域。
else if (mutex.WaitOne(10000,false))//
{
try
{
Run();
}
finally
{
mutex.ReleaseMutex();
}
}
else//如果沒有發現處理信号
{
Console.WriteLine("已經有執行個體了。");
Console.ReadLine();
}
}
static void Run()
{
Console.WriteLine("執行個體1");
Console.ReadLine();
}
複制
順序啟動A B執行個體測試下。A首先拿到鎖,輸出 執行個體1 。B在等待, 如果10秒内A釋放,B拿到執行Run()。逾時後輸出"已經有執行個體了"。
這裡注意的是第一個拿到處理信号 的執行個體,已經拿到鎖了。不需要再WaitOne。 否則報異常。
Semaphore
即信号量,我們可以把它了解為更新版的mutex。mutex對一個資源進行鎖,semaphore則是對多個資源進行加鎖。
semaphore是由windows核心維持一個int32變量的線程計數器,線程每調用一次、計數器減一、釋放後對應加一, 超出的線程則排隊等候。
走的是核心構造,是以semaphore也是可以跨程序的。
static void Main(string[] args)
{
Console.WriteLine("準備處理隊列");
bool createNew = false;
SemaphoreSecurity ss = new SemaphoreSecurity(); //信号量權限控制
Semaphore semaphore = new Semaphore(2, 2, "mushroom.Semaphore", out createNew,null);
for (int i = 1; i <= 5; i++)
{
new Thread((arg) =>
{
semaphore.WaitOne();
Console.WriteLine(arg + "進行中");
Thread.Sleep(10000);
semaphore.Release(); //即semaphore.Release(1)
//semaphore.Release(5);可以釋放多個,但不能超過最大值。如果最後釋放的總量超過本身總量,也會報錯。 不建議使用
}).Start(i);
}
Console.ReadLine();
}
複制
總結
mutex、Semaphore 需要先把托管代碼轉成本地使用者模式代碼、再轉換成本地核心代碼。
當釋放後需要重新轉換成托管代碼,性能會有一定的損耗,是以盡量在需要跨程序的場景再使用。
參考 http://www.cnblogs.com/artech/archive/2007/06/04/769805.html