閱讀683 返回首頁    go 技術社區[雲棲]


[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)]被應用到,相當於對加鎖。
  • 如果[MethodImplAttribute(MethodImplOptions.Synchronized)]被應用到,相當於當前類型加鎖

為了驗證我們上麵提出的結論,我作了一個小小的例子。在一個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:     static void 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被執行一次。這一點從最終執行的結果可以看出:

image

為了讓同一個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方法執行的時間:

image

在一開始我們提出的結論中,我們提到“如果[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。而輸出的結果證實了這點:

image

討論完再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的間隔異步地執行,最終的輸出結果如下:

image

然後我們將對[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將會按照我們期望的那樣以同步的方式執行,執行的間隔正是方法執行的時間:

image

我們回顧一下第三個結論:“如果[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的鎖釋放後才能執行。所以如果上述的結論成立,將會有下麵的輸出:

image

對於加鎖來說,鎖的粒度的選擇顯得至關重要。在不同的場景中需要選擇不同粒度的鎖。如果選擇錯誤往往會對性能造成很到的影響,嚴重時還會引起死鎖。就拿[MethodImplAttribute(MethodImplOptions.Synchronized)]來說,如果開發人員對它的實現機製不了解,很有可能使它lock(this)或者lock(typeof(…))並存,造成方法得不到及時地執行。

最後說一句題外話,因為字符串駐留機製的存在,切忌對string進行加鎖。



作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-30 16:34:14

  上一篇:go  Enterprise Library深入解析與靈活應用(4):創建一個自定義Exception Handler改變ELAB的異常處理機製
  下一篇:go  New Release: patterns & practices App Arch Guide 2.0 Beta 1