[原創][MethodImpl(MethodImplOptions.Synchronized)]、lock(this)與lock(typeof(...))
對於稍微有點經驗的.NET開發人員來說,倘若被問及如何保持執行緒同步,我想很多人都能說好好幾種。在眾多的執行緒同步的可選方式中,加鎖無疑是最為常用的。如果僅僅是基於方法級別的執行緒同步,使用System.Runtime.CompilerServices.MethodImplAttribute無疑是最為簡潔的一種方式。MethodImplAttribute可以用於instance method,也可以用於static method。當在某個方法上標註了MethodImplAttribute,並指定MethodImplOptions.Synchronized引數,可以確保在不同執行緒中執行的該方式以同步的方式執行。我們幾天來討論MethodImplAttribute(MethodImplOptions.Synchronized)和lock的關係。
一、提出結論
在進行討論之前,我先提出下面3個結論:
- [MethodImplAttribute(MethodImplOptions.Synchronized)]仍然採用加鎖的機制實現執行緒的同步。
- 如果[MethodImplAttribute(MethodImplOptions.Synchronized)]被應用到instance method,相當於對當前例項加鎖。
- 如果[MethodImplAttribute(MethodImplOptions.Synchronized)]被應用到static method,相當於當前型別加鎖
二、基於instance method的執行緒同步
為了驗證我們上面提出的結論,我作了一個小小的例子。在一個console application中定義了一個class:SyncHelper,其中定義了一個方法Execute。打印出方法執行的時間,並休眠當前執行緒模擬一個耗時的操作:
1: class SyncHelper
2: {
3: public void Execute()
4: {
5: Console.WriteLine("Excute at {0}", DateTime.Now);
6: Thread.Sleep(5000);
7: }
8: }
在入口Main方法中,建立SyncHelper物件,通過一個System.Threading.Timer物件實現每隔1s呼叫該物件的Execute方法:
1: class Program
2: {
3: staticvoid Main(string[] args)
4: {
5: SyncHelper helper = new SyncHelper();
6: Timer timer = new Timer(
7: delegate
8: {
9: helper.Execute();
10: }, null, 0, 1000);
11:
12: Console.Read();
13:
14: }
15: }
16:
由於Timer物件採用非同步的方式進行呼叫,所以雖然Execute方法的執行時間是5s,但是該方法仍然是每隔1s被執行一次。這一點從最終執行的結果可以看出:
為了讓同一個SyncHelper物件的Execute方法同步執行,我們在Execute方法上添加了如下一個MethodImplAttribute:
1: [MethodImpl(MethodImplOptions.Synchronized)]
2: public void Execute()
3: {
4: Console.WriteLine("Excute at {0}", DateTime.Now);
5: Thread.Sleep(5000);
6: }
從如下的輸出結果我們可以看出Execute方法是以同步的方式執行的,因為兩次執行的間隔正式Execute方法執行的時間:
在一開始我們提出的結論中,我們提到“如果[MethodImplAttribute(MethodImplOptions.Synchronized)]被應用到instance method,相當於對當前例項加鎖”。說得直白一點:[MethodImplAttribute(MethodImplOptions.Synchronized)] = lock(this)。我們可以通過下面的實驗驗證這一點。為此,在SyncHelper中定義了一個方法LockMyself。在此方法中對自身加鎖,並持續5s中,並答應加鎖和解鎖的時間。
1: public void LockMyself()
2: {
3: lock (this)
4: {
5: Console.WriteLine("Lock myself at {0}", DateTime.Now);
6: Thread.Sleep(5000);
7: Console.WriteLine("Unlock myself at {0}", DateTime.Now);
8: }
9: }
我們在Main()中以非同步的方式(通過建立新的執行緒的方式)呼叫該方法:
1: static void Main(string[] args)
2: {
3: SyncHelper helper = new SyncHelper();
4:
5: Thread thread = new Thread(
6: delegate()
7: {
8:
9: helper.LockMyself();
10:
11: });
12: thread.Start();
13: Timer timer = new Timer(
14: delegate
15: {
16: helper.Execute();
17: }, null, 0, 1000);
18:
19: Console.Read();
20: }
結合我們的第二個結論想想最終的輸出會是如何。由於LockMyself方法是在另一個執行緒中執行,我們可以簡單講該方法的執行和Execute的第一個次執行看作是同時的。但是MethodImplAttribute(MethodImplOptions.Synchronized)]果真是通過lock(this)的方式實現的話,Execute必須在等待LockMyself方法執行結束將對自身的鎖釋放後才能得以執行。也就是說LockMyself和第一次Execute方法的執行應該相差5s。而輸出的結果證實了這點:
三、基於static method的執行緒同步
討論完再instance method上新增MethodImplAttribute(MethodImplOptions.Synchronized)]的情況,我們相同的方式來討論倘若一樣的MethodImplAttribute被應用到static方法,又會使怎樣的結果。
我們先將Execute方法上的MethodImplAttribute註釋掉,並將其改為static方法:
1: //[MethodImpl(MethodImplOptions.Synchronized)]
2: public static void Execute()
3: {
4: Console.WriteLine("Excute at {0}", DateTime.Now);
5: Thread.Sleep(5000);
6: }
在Main方法中,通過Timer呼叫該static方法:
1: static void Main(string[] args)
2: {
3: Timer timer = new Timer(
4: delegate
5: {
6: SyncHelper.Execute();
7: }, null, 0, 1000);
8:
9: Console.Read();
10: }
毫無疑問,Execute方法將以1s的間隔非同步地執行,最終的輸出結果如下:
然後我們將對[MethodImpl(MethodImplOptions.Synchronized)]的註釋取消:
1: [MethodImpl(MethodImplOptions.Synchronized)]
2: public static void Execute()
3: {
4: Console.WriteLine("Excute at {0}", DateTime.Now);
5: Thread.Sleep(5000);
6: }
最終的輸出結果證實了Execute將會按照我們期望的那樣以同步的方式執行,執行的間隔正是方法執行的時間:
我們回顧一下第三個結論:“如果[MethodImplAttribute(MethodImplOptions.Synchronized)]被應用到static method,相當於當前型別加鎖”。為了驗證這個結論,在SyncHelper中添加了一個新的static方法:LockType。該方法對SyncHelper tpye加鎖,並持續5s中,在加鎖和解鎖是打印出當前時間:
1: public static void LockType()
2: {
3: lock (typeof(SyncHelper))
4: {
5: Console.WriteLine("Lock SyncHelper type at {0}", DateTime.Now);
6: Thread.Sleep(5000);
7: Console.WriteLine("Unlock SyncHelper type at {0}", DateTime.Now);
8: }
9: }
在Main中,像驗證instance method一樣,建立新的執行緒執行LockType方法:
1: static void Main(string[] args)
2: {
3: Thread thread = new Thread(
4: delegate()
5: {
6: SyncHelper.LockType();
7: });
8: thread.Start();
9:
10: Timer timer = new Timer(
11: delegate
12: {
13: SyncHelper.Execute();
14: }, null, 0, 1000);
15:
16: Console.Read();
17: }
18:
如果基於static method的[MethodImplAttribute(MethodImplOptions.Synchronized)]是通過對Type進行加鎖實現。那麼通過Timer輪詢的第一個Execute方法需要在LockType方法執行完成將對SyncHelper type的鎖釋放後才能執行。所以如果上述的結論成立,將會有下面的輸出:
四、總結
對於加鎖來說,鎖的粒度的選擇顯得至關重要。在不同的場景中需要選擇不同粒度的鎖。如果選擇錯誤往往會對效能造成很到的影響,嚴重時還會引起死鎖。就拿[MethodImplAttribute(MethodImplOptions.Synchronized)]來說,如果開發人員對它的實現機制不瞭解,很有可能使它lock(this)或者lock(typeof(…))並存,造成方法得不到及時地執行。
最後說一句題外話,因為字串駐留機制的存在,切忌對string進行加鎖。