最近一個月一直在弄文件傳輸組件,其中用到多線程的技術,但有的地方確實需要只能有一個線程來操作,如何才能保證只有一個線程呢?首先想到的就是鎖的概念,最近在我們項目組中聽的最多的也是鎖誰,如何鎖?看到有同事使用lock(this),也有lock(private static object),那就有點困惑了,lock到底鎖誰才是最合適的呢?
首先先上官方Msdn的說法
lock 關鍵字可確保當一個線程位於代碼的臨界區時,另一個線程不會進入該臨界區。 如果其他線程嘗試進入鎖定的代碼,則它將一直等待(即被阻止),直到該對象被釋放。
lock 關鍵字在塊的開始處調用 Enter,而在塊的結尾處調用 Exit。 ThreadInterruptedException 引發,如果 Interrupt 中斷等待輸入 lock 語句的線程。
通常,應避免鎖定 public 類型,否則實例將超出代碼的控制范圍。常見的結構 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 違反此准則:
如果實例可以被公共訪問,將出現 lock (this) 問題。
如果 MyType 可以被公共訪問,將出現 lock (typeof (MyType)) 問題。
由於進程中使用同一字符串的任何其他代碼都將共享同一個鎖,所以出現 lock("myLock") 問題。
最佳做法是定義 private 對象來鎖定, 或 private static 對象變量來保護所有實例所共有的數據。
在 lock 語句的正文不能使用 等待 關鍵字。
Enter指的是Monitor.Enter(獲取指定對象上的排他鎖。),Exit指的是Monitor.Exit(釋放指定對象上的排他鎖。)
有上面msdn的解釋及Exit方法,可以這樣猜測“直到該對象被釋放”,”該對象“應該是指鎖的對象,對象釋放了或者對象改變了,其他的線程才可以進入代碼臨界區(是不是可以這樣來理解?)。
在多線程中,每個線程都有自己的資源,但是代碼區是共享的,即每個線程都可以執行相同的函數。這可能帶來的問題就是幾個線程同時執行一個函數,導致數據的混亂,產生不可預料的結果,因此我們必須避免這種情況的發生。
打個比方,有這樣一個情景,很多公司所在的大廈的廁所的蹲位都是小單間型的,也就是一次只能進去一個人,那麼為了避免每次進去一個人,那怎麼做呢?不就是一個人進去之後順手把門鎖上麼?這樣你在裡面干啥事,外邊的人也只能等待你解放完了,才能進入。而蹲位的資源(蹲位,手紙等)是共享的。
最常使用的鎖是如下格式的代碼段:
private static object objlock = new object();
lock (objlock )
{
//要執行的代碼邏輯
}
為什麼鎖的對象是私有的呢?還是以廁所為例子吧,私有就好比,這把鎖只有你能訪問到,而且最好這把鎖不會因為外力而有所改變,別人訪問不到,這樣才能保證你進去了,別人就進不去了,如果是公有的,就好比你蹲位小單間的鎖不是安裝在裡面而是安裝在外邊的,別人想不想進就不是你所能控制的了,這樣也不安全。
通過字面的意思就是鎖的當前實例對象。那是否對其他實例對象產生影響?那下面看一個例子:
1 namespace Wolfy.LockDemo
2 {
3 class Program
4 {
5 static void Main(string[] args)
6 {
7 Test t = new Test();
8 Test t2 = new Test();
9 Thread[] threads = new Thread[10];
10 for (int i = 0; i < threads.Length; i++)
11 {
12 //通過循環創建10個線程。
13 threads[i] = new Thread(() =>
14 {
15 t2.Print();
16 });
17 //為每個線程設置一個名字
18 threads[i].Name = "thread" + i;
19
20 }
21 //開啟創建的十個線程
22 for (int i = 0; i < threads.Length; i++)
23 {
24 threads[i].Start();
25 }
26
27 Console.Read();
28 }
29 }
30 class Test
31 {
32 public void Print()
33 {
34 lock (this)
35 {
36 for (int i = 0; i < 5; i++)
37 {
38 Console.WriteLine("\t" + Thread.CurrentThread.Name.ToString() + "\t" + i.ToString() + " ");
39 }
40 }
41 }
42 }
43 }
如果在不加鎖的情況下輸出如下:

從上面的輸出結果也可以看出,線程出現了爭搶的現象,而這並不是我們想要的結果,我們想要的是,每次只有一個線程去執行Print方法。那我們就嘗試一下lock(this)
1 class Test
2 {
3 public void Print()
4 {
5 lock (this)
6 {
7 for (int i = 0; i < 5; i++)
8 {
9 Console.WriteLine("\t" + Thread.CurrentThread.Name.ToString() + "\t" + i.ToString() + " ");
10 }
11 }
12 }
13 }
輸出結果

從輸出結果,覺得大功告成了,可是現在情況又來了,在項目中的其他的地方,有同事也這樣寫了這樣的代碼,又創建了一個Test對象,而且他也知道使用多線程執行耗時的工作,那麼就會出現類似下面的代碼。
1 namespace Wolfy.LockDemo
2 {
3 class Program
4 {
5 static void Main(string[] args)
6 {
7 Test t = new Test();
8 Test t2 = new Test();
9 t2.Age = 20;
10 Thread[] threads = new Thread[10];
11 for (int i = 0; i < threads.Length; i++)
12 {
13 //通過循環創建10個線程。
14 threads[i] = new Thread(() =>
15 {
16 t.Print();
17 t2.Print();
18 });
19 //為每個線程設置一個名字
20 threads[i].Name = "thread" + i;
21
22 }
23
24
25 //開啟創建的十個線程
26 for (int i = 0; i < threads.Length; i++)
27 {
28 threads[i].Start();
29 }
30
31 Console.Read();
32 }
33 }
34 class Test
35 {
36 public int Age { get; set; }
37 public void Print()
38 {
39 lock (this)
40 {
41 for (int i = 0; i < 5; i++)
42 {
43 Console.WriteLine("\t" + Thread.CurrentThread.Name.ToString() + "\t" + i.ToString() + " ");
44 }
45 }
46 }
47 }
48 }
這裡為Test加了一個Age屬性,為了區別當前創建的對象不是同一個對象。
輸出的結果為

在輸出的結果中已經出現了線程搶占執行的情況了,而不是一個線程執行完另一個線程在執行。
那麼我們現在使用一個全局的私有的對象試一試。
1 namespace Wolfy.LockDemo
2 {
3 class Program
4 {
5 private static object objLock = new object();
6 static void Main(string[] args)
7 {
8 Test t = new Test();
9 Test t2 = new Test();
10 t2.Age = 20;
11 Thread[] threads = new Thread[10];
12 for (int i = 0; i < threads.Length; i++)
13 {
14 //通過循環創建10個線程。
15 threads[i] = new Thread(() =>
16 {
17 lock (objLock)
18 {
19 t.Print();
20 t2.Print();
21 }
22 });
23 //為每個線程設置一個名字
24 threads[i].Name = "thread" + i;
25
26 }
27
28
29 //開啟創建的十個線程
30 for (int i = 0; i < threads.Length; i++)
31 {
32 threads[i].Start();
33 }
34
35 Console.Read();
36 }
37 }
38 class Test
39 {
40 public int Age { get; set; }
41 public void Print()
42 {
43 for (int i = 0; i < 5; i++)
44 {
45 Console.WriteLine("\t" + Thread.CurrentThread.Name.ToString() + "\t" + i.ToString() + " ");
46 }
47 }
48 }
49 }
輸出的結果

從輸出的結果也可以看出,有序的,每次進來一個線程執行。
那通過上面的比較可以有這樣的一個結論,lock的結果好不好,還是關鍵看鎖的誰,如果外邊能對這個誰進行修改,lock就失去了作用。所以一般情況下,使用靜態的並且是只讀的對象。
也就有了類似下面的代碼
1 private static readonly object objLock = new object();
你可能會說,不對啊,你下面的代碼跟上面的代碼不一樣啊,為什麼就得出這樣的結論?難道就不能把Object放在test類中麼,放在test類中的話,在new Test()的時候,其實放在Test中也是可以的,只要保證objLock在外部是無法修改的就可以。
上面說的最多的是lock對象,那麼它能不能lock值類型?
答案是否定的,如

當然lock(null)也是不行的,如圖

雖然編譯可以通過,但是運行就會出錯。
string也是應用類型,從語法上來說是沒有錯的。
但是鎖定字符串尤其危險,因為字符串被公共語言運行庫 (CLR)“暫留”。 這意味著整個程序中任何給定字符串都只有一個實例,就是這同一個對象表示了所有運行的應用程序域的所有線程中的該文本。因此,只要在應用程序進程中的任何位置處具有相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的所有實例。通常,最好避免鎖定 public 類型或鎖定不受應用程序控制的對象實例。例如,如果該實例可以被公開訪問,則 lock(this) 可能會有問題,因為不受控制的代碼也可能會鎖定該對象。這可能導致死鎖,即兩個或更多個線程等待釋放同一對象。出於同樣的原因,鎖定公共數據類型(相比於對象)也可能導致問題。而且lock(this)只對當前對象有效,如果多個對象之間就達不到同步的效果。lock(typeof(Class))與鎖定字符串一樣,范圍太廣了。
關於lock的介紹就到這裡,有下面幾點需要注意的地方
1、lock的是引用類型的對象,string類型除外。
2、lock推薦的做法是使用靜態的、只讀的、私有的對象。
3、保證lock的對象在外部無法修改才有意義,如果lock的對象在外部改變了,對其他線程就會暢通無阻,失去了lock的意義。
參考文章
http://www.cnblogs.com/jintianhu/archive/2010/11/19/1881494.html