在這篇文章中,我們學習理解lock關鍵詞、monitor類、mutex互斥鎖和semaphore信號量的應用。
所有的這些類,如lock/monitor/mutex/semaphore,在多線程應用中,提供了一種同步機制來保護共享的代碼或資源。
C# lock關鍵詞
C#關鍵詞:在C#中,鎖lock是一種同步機制,允許在同一時間只允許一個線程訪問指定的代碼或區域。在多線程環境中,lock主要用於在讀寫公共變量的時候,執行排它鎖(exclusive locks)來避免產生不一致的結果。
一般來說,lock用於關鍵代碼段(critical section),那里同一時間內只允許一個線程訪問資源,其它線程是鎖住的,無法獲得互斥鎖,必須要等待互斥鎖的釋放。
lock關鍵詞的語法
為指定代碼段獲取互斥鎖(mutual-exclusion)的lock語法如下:
Lock (expression) { statement block }
1 //syntax to use the lock keyword 2 lock (obj) 3 { 4 // Critical code section 5 }
在C#中,lock關鍵詞確保同一時間只有一個線程在運行。當另一個線程已經在運行的時候,lock關鍵詞會阻止其它線程進入關鍵代碼段。
在下面的示例中,我們在關鍵代碼段中使用lock關鍵詞。lock只允許同一時間內一個線程進入並執行代碼。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace TheLock 8 { 9 class Program 10 { 11 static readonly object _lock = new object(); 12 static void Main(string[] args) 13 { 14 Thread thread1 = new Thread(PrintCharacter); 15 Thread thread2 = new Thread(PrintCharacter); 16 17 thread1.Start(); 18 thread2.Start(); 19 20 Console.ReadLine(); 21 } 22 23 public static void PrintCharacter() 24 { 25 string strArray = "Hello World!"; 26 lock (_lock) 27 { 28 for (int i = 0; i < strArray.Length; i++) 29 { 30 Console.Write(strArray[i]); 31 Thread.Sleep(TimeSpan.FromSeconds(1)); 32 } 33 } 34 Console.Write(" "); 35 } 36 } 37 }
在上面 的代碼中,我們在主方法main中創建了兩個獨立線程thread1和thread2,同時調用靜態方法“PrintCharacter”。
這里,我們在變量"_lock"上使用lock關鍵詞在 for 循環語句上獲得排斥鎖。
它只允許一個線程進入關鍵代碼段去執行代碼。
運行結果見上圖。從上面 的結果可以看到,lock語句等待第一個線程釋放了“_lock”后,才讓第二個線程執行此段代碼。
沒有Lock關鍵詞的多線程示例
在下面的示例中,多線程在沒有lock的情況下同時執行關鍵代碼段的代碼。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace TheLock 8 { 9 class Program 10 { 11 static readonly object _lock = new object(); 12 static void Main(string[] args) 13 { 14 Thread thread1 = new Thread(PrintCharacter); 15 Thread thread2 = new Thread(PrintCharacter); 16 17 thread1.Start(); 18 thread2.Start(); 19 20 Console.ReadLine(); 21 } 22 23 public static void PrintCharacter() 24 { 25 string strArray = "Hello World!"; 26 //lock (_lock) 27 //{ 28 for (int i = 0; i < strArray.Length; i++) 29 { 30 Console.Write(strArray[i]); 31 Thread.Sleep(TimeSpan.FromSeconds(1)); 32 } 33 //} 34 Console.WriteLine(); 35 } 36 } 37 }
在上面結果中,我們看到thread1和thread2同時執行了相同的代碼段。
這就是為什么,當我們在多線程環境中讀寫公共變量的時候,我們要用lock執行排它鎖來避免產生不一致的結果。
在C#中,lock語句內部包含Monitor.Enter方法和Monitor.Exit方法,還有try/finally語句塊。
lock = Monitor.Enter + Monitor.Exit + try/finally
在上面 的項目中,我們用object實例做為lock語句的參數。但是,如果我們在lock語句中不是使用object類型,而是使用值類型會發生什么呢。很不幸,C#編譯器將會拋出編譯錯誤信息,如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace TheLock 8 { 9 class Program 10 { 11 static int _lock ; 12 static void Main(string[] args) 13 { 14 Thread thread1 = new Thread(PrintCharacter); 15 Thread thread2 = new Thread(PrintCharacter); 16 17 thread1.Start(); 18 thread2.Start(); 19 20 Console.ReadLine(); 21 } 22 23 public static void PrintCharacter() 24 { 25 string strArray = "Hello World!"; 26 lock (_lock) 27 { 28 for (int i = 0; i < strArray.Length; i++) 29 { 30 Console.Write(strArray[i]); 31 Thread.Sleep(TimeSpan.FromSeconds(1)); 32 } 33 } 34 Console.WriteLine(); 35 } 36 } 37 }
‘int’ is not a reference type as required by the lock statement.
翻譯:“int”不是lock語句要求的引用類型
避免在下列情況下使用lock關鍵詞
避免lock關鍵詞用於值類型;
避免將“this”用於lock表達式;
不要鎖定的類型
1、避免lock關鍵詞用於值類型;
如果編譯器允許lock值類型,每次都會lock到值類型的不同裝箱副本,最后也沒有lock到任何東西。結果就是,lock認為值類型是很多不同的對象。lock關鍵詞用於引用類型而不是值類型,否則,編譯器會報錯。
2、避免將“this”用於lock表達式;
使用私有引用類型變量,而不是this,在多線程等待開啟相同對象的情況下可以避免死鎖。使用this做為lock語句參數是個不好的習慣,因為它會阻止整個對象,其它的代碼也會被阻塞,沒有理由地等待執行,還可能導致運行性能問題。
使用this會導致很多問題,因為實例是公開可訪問的,會被其它很多進程訪問。
3、避免在string對象上使用lock關鍵詞
避免在string對象上使用lock關鍵詞是因為字符串被公共語言運行庫 (CLR)“暫留”。 這意味着整個程序中任何給定字符串都只有一個實例,就是這同一個對象表示了所有運行的應用程序域的所有線程中的該文本。因此,只要在應用程序進程中的任何位置處具有相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的所有實例。沒有足夠的知識儲備,會導致死鎖。