平常在多線程開發中,總避免不了線程同步。本篇就對net多線程中的鎖系統做個簡單描述。
目錄
一:lock、Monitor
1:基礎。
2: 作用域。
3:字符串鎖。
4:monitor使用
二:mutex
三:Semaphore
四:總結
一:lock、Monitor
1:基礎
Lock是Monitor語法糖簡化寫法。Lock在IL會生成Monitor。
isGetLock參數是Framework 4.0后新加的。 為了使程序在所有情況下都能夠確定,是否有必要釋放鎖。例: Monitor.Enter拿不到鎖
Monitor.Enter 是可以鎖值類型的。鎖時會裝箱成新對象,所以無法做到線程同步。
2:作用域
一:Lock是只能在進程內鎖,不能跨進程。走的是混合構造,先自旋再轉成內核構造。
二:關于對type類型的鎖。如下:
運行結果如下:
我們在來看個例子。
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中的基礎類型都是這樣。 極容易造成死鎖,慎用。 而自定義類型則會加載到自己的程序域,不會影響別人。
3:字符串的鎖
我們都知道鎖的目的,是為了多線程下值被破壞。也知道string在c#是個特殊對象,值是不變的,每次變動都是一個新對象值,這也是推薦stringbuilder原因。如例:
正式由于c#中字符串的這種特性,所以字符串是在多線程下是不會被修改的,只讀的。它存在于SystemDomain域中managed heap中的一個hash table中。Key為string本身,Value為string對象的地址。
當程序域需要一個string的時候,CLR首先在這個Hashtable根據這個string的hash code試著找對應的Item。如果成功找到,則直接把對應的引用返回,否則就在SystemDomain對應的managed heap中創建該 string,并加入到hash table中,并把引用返回。所以說字符串的生命周期是基于整個進程的,也是跨AppDomain。
4:monitor用法
介紹下Wait,Pulse,PulseAll的用法。有注釋,大家直接看代碼吧。
}).Start();
Console.ReadLine();
二:mutex
lock是不能跨進程鎖的。 mutex作用和lock類似,但是它能跨進程鎖資源(走的是windows內核構造)。 我們來看個例子
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也是可以跨進程的。
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 需要由托管代碼轉成本地用戶模式代碼、再轉換為本地內核代碼。
反之同樣,饒了一大圈,性能肯定不會很好。所以僅在需要跨進程的場景才使用。
新聞熱點
疑難解答