一、Lock定義
lock 關鍵字可以用來確保程式碼塊完成執行,而不會被其他執行緒中斷。它可以把一段程式碼定義為互斥段(critical section),互斥段在一個時刻內只允許一個執行緒進入執行,而其他執行緒必須等待。這是透過在程式碼塊執行期間為給定物件獲取互斥鎖來實現的。
在多執行緒中,每個執行緒都有自己的資源,但是程式碼區是共享的,即每個執行緒都可以執行相同的函式。這可能帶來的問題就是幾個執行緒同時執行一個函式,導致資料的混亂,產生不可預料的結果,因此我們必須避免這種情況的發生。
而在.NET中最好了解一下程序、應用域和執行緒的概念,因為Lock是針對執行緒一級的,而在.NET中應用域是否會對Lock起隔離作用,我的猜想是,即不在同一應用域中的執行緒無法透過Lock來中斷;另外也最好能瞭解一下資料段、程式碼段、堆、棧等概念。
在C# lock關鍵字定義如下:
lock(expression) statement_block,其中expression代表你希望跟蹤的物件,通常是物件引用。
如果你想保護一個類的例項,一般地,你可以使用this;如果你想保護一個靜態變數(如互斥程式碼段在一個靜態方法內部),一般使用類名就可以了。
而statement_block就是互斥段的程式碼,這段程式碼在一個時刻內只可能被一個執行緒執行。
二、簡單例子
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
namespace ConsoleApplication1
{
class Program
static void Main(string[] args)
Thread thread1 = new Thread(new ThreadStart(ThreadStart1));
thread1.Name = "Thread1";
Thread thread2 = new Thread(new ThreadStart(ThreadStart2));
thread2.Name = "Thread2";
Thread thread3 = new Thread(new ThreadStart(ThreadStart3));
thread3.Name = "Thread3";
thread1.Start();
thread2.Start();
thread3.Start();
Console.ReadKey();
}
static object _object = new object();
static void Done(int millisecondsTimeout)
Console.WriteLine(string.Format("{0} -> {1}.Start", DateTime.Now.ToString("HH:mm:ss"), Thread.CurrentThread.Name));
//下邊程式碼段同一時間只能由一個執行緒在執行
lock (_object)
Console.WriteLine(string.Format("{0} -> {1}進入鎖定區域.", DateTime.Now.ToString("HH:mm:ss"), Thread.CurrentThread.Name));
Thread.Sleep(millisecondsTimeout);
Console.WriteLine(string.Format("{0} -> {1}退出鎖定區域.", DateTime.Now.ToString("HH:mm:ss"), Thread.CurrentThread.Name));
static void ThreadStart1()
Done(5000);
static void ThreadStart2()
Done(3000);
Done(1000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
三、簡單解釋一下執行過程
先來看看執行過程,程式碼示例如下:
private static object ojb = new object();
lock(obj)
//鎖定執行的程式碼段
假設執行緒A先執行,執行緒B稍微慢一點。執行緒A執行到lock語句,判斷obj是否已申請了互斥鎖,判斷依據是逐個與已存在的鎖進行object.ReferenceEquals比較(此處未加證實),如果不存在,則申請一個新的互斥鎖,這時執行緒A進入lock裡面了。
這時假設執行緒B啟動了,而執行緒A還未執行完lock裡面的程式碼。執行緒B執行到lock語句,檢查到obj已經申請了互斥鎖,於是等待;直到執行緒A執行完畢,釋放互斥鎖,執行緒B才能申請新的互斥鎖並執行lock裡面的程式碼。
四、Lock的物件選擇問題
接下來說一些lock應該鎖定什麼物件。
1、為什麼不能lock值型別
比如lock(1)呢?lock本質上Monitor.Enter,Monitor.Enter會使值型別裝箱,每次lock的是裝箱後的物件。lock其實是類似編譯器的語法糖,因此編譯器直接限制住不能lock值型別。退一萬步說,就算能編譯器允許你lock(1),但是object.ReferenceEquals(1,1)始終返回false(因為每次裝箱後都是不同物件),也就是說每次都會判斷成未申請互斥鎖,這樣在同一時間,別的執行緒照樣能夠訪問裡面的程式碼,達不到同步的效果。同理lock((object)1)也不行。
2、Lock字串
那麼lock(“xxx”)字串呢?MSDN上的原話是:
鎖定字串尤其危險,因為字串被公共語言執行庫 (CLR)“暫留”。 這意味著整個程式中任何給定字串都只有一個例項,就是這同一個物件表示了所有執行的應用程式域的所有執行緒中的該文字。因此,只要在應用程式程序中的任何位置處具有相同內容的字串上放置了鎖,就將鎖定應用程式中該字串的所有例項。
3、MSDN推薦的Lock物件
通常,最好避免鎖定 public 型別或鎖定不受應用程式控制的物件例項。例如,如果該例項可以被公開訪問,則 lock(this) 可能會有問題,因為不受控制的程式碼也可能會鎖定該物件。這可能導致死鎖,即兩個或更多個執行緒等待釋放同一物件。出於同樣的原因,鎖定公共資料型別(相比於物件)也可能導致問題。
而且lock(this)只對當前物件有效,如果多個物件之間就達不到同步的效果。
而自定義類推薦用私有的只讀靜態物件,比如:
private static readonly object obj = new object();
為什麼要設定成只讀的呢?這時因為如果在lock程式碼段中改變obj的值,其它執行緒就暢通無阻了,因為互斥鎖的物件變了,object.ReferenceEquals必然返回false。
4、lock(typeof(Class))
與鎖定字串一樣,範圍太廣了。
五、特殊問題:Lock(this)等的詳細解釋
在以前程式設計中遇到lock問題總是使用lock(this)一鎖了之,出問題後翻看MSDN突然發現下面幾行字:通常,應避免鎖定 public 型別,否則例項將超出程式碼的控制範圍。常見的結構 lock (this)、lock (typeof (MyType)) 和 lock (“myLock”) 違反此準則:如果例項可以被公共訪問,將出現C# lock this問題。如果 MyType 可以被公共訪問,將出現 lock (typeof (MyType)) 問題。由於程序中使用同一字串的任何其他程式碼將共享同一個鎖,所以出現 lock(“myLock”) 問題。
來看看C# lock this問題:如果有一個類Class1,該類有一個方法用lock(this)來實現互斥:
publicvoidMethod2()
lock(this)
System.Windows.Forms.MessageBox.Show(“Method2End”);
如果在同一個Class1的例項中,該Method2能夠互斥的執行。但是如果是2個Class1的例項分別來執行Method2,是沒有互斥效果的。因為這裡的lock,只是對當前的例項物件進行了加鎖。
Lock(typeof(MyType))鎖定住的物件範圍更為廣泛,由於一個類的所有例項都只有一個型別物件(該物件是typeof的返回結果),鎖定它,就鎖定了該物件的所有例項,微軟現在建議,不要使用lock(typeof(MyType)),因為鎖定型別物件是個很緩慢的過程,並且類中的其他執行緒、甚至在同一個應用程式域中執行的其他程式都可以訪問該型別物件,因此,它們就有可能代替您鎖定型別物件,完全阻止您的執行,從而導致你自己的程式碼的掛起。
鎖住一個字串更為神奇,只要字串內容相同,就能引起程式掛起。原因是在.NET中,字串會被暫時存放,如果兩個變數的字串內容相同的話,.NET會把暫存的字串物件分配給該變數。所以如果有兩個地方都在使用lock(“my lock”)的話,它們實際鎖住的是同一個物件。到此,微軟給出了個lock的建議用法:鎖定一個私有的static 成員變數。
.NET在一些集合類中(比如ArrayList,HashTable,Queue,Stack)已經提供了一個供lock使用的物件SyncRoot,用Reflector工具查看了SyncRoot屬性的程式碼,在Array中,該屬性只有一句話:return this,這樣和lock array的當前例項是一樣的。ArrayList中的SyncRoot有所不同
get
if(this._syncRoot==null)
Interlocked.CompareExchange(refthis._syncRoot,newobject(),null);
returnthis._syncRoot;
其中Interlocked類是專門為多個執行緒共享的變數提供原子操作(如果你想鎖定的物件是基本資料型別,那麼請使用這個類),CompareExchange方法將當前syncRoot和null做比較,如果相等,就替換成new object(),這樣做是為了保證多個執行緒在使用syncRoot時是執行緒安全的。集合類中還有一個方法是和同步相關的:Synchronized,該方法返回一個對應的集合類的wrapper類,該類是執行緒安全的,因為他的大部分方法都用lock來進行了同步處理,比如Add方法:
publicoverridevoidAdd(objectkey,objectvalue)
lock(this._table.SyncRoot)
this._table.Add(key,value);
這裡要特別注意的是MSDN提到:從頭到尾對一個集合進行列舉本質上並不是一個執行緒安全的過程。即使一個集合已進行同步,其他執行緒仍可以修改該集合,這將導致列舉數引發異常。若要在列舉過程中保證執行緒安全,可以在整個列舉過程中鎖定集合:
QueuemyCollection=newQueue();
lock(myCollection.SyncRoot){
foreach(ObjectiteminmyCollection){
//Insertyourcodehere.
最後
注意:應避免鎖定 public 型別,否則例項將超出程式碼的控制範圍。常見的結構 lock (this)、lock (typeof (MyType)) 和 lock (“myLock”) 違反此準則:
1)如果例項可以被公共訪問,將出現 lock (this) 問題;
2)如果 MyType 可以被公共訪問,將出現 lock (typeof (MyType)) 問題;
3)由於程序中使用同一字串的任何其他程式碼將共享同一個鎖,所以出現 lock(“myLock”) 問題;
最佳做法是定義 private 物件來鎖定, 或 private static 物件變數來保護所有例項所共有的資料。
一、Lock定義
lock 關鍵字可以用來確保程式碼塊完成執行,而不會被其他執行緒中斷。它可以把一段程式碼定義為互斥段(critical section),互斥段在一個時刻內只允許一個執行緒進入執行,而其他執行緒必須等待。這是透過在程式碼塊執行期間為給定物件獲取互斥鎖來實現的。
在多執行緒中,每個執行緒都有自己的資源,但是程式碼區是共享的,即每個執行緒都可以執行相同的函式。這可能帶來的問題就是幾個執行緒同時執行一個函式,導致資料的混亂,產生不可預料的結果,因此我們必須避免這種情況的發生。
而在.NET中最好了解一下程序、應用域和執行緒的概念,因為Lock是針對執行緒一級的,而在.NET中應用域是否會對Lock起隔離作用,我的猜想是,即不在同一應用域中的執行緒無法透過Lock來中斷;另外也最好能瞭解一下資料段、程式碼段、堆、棧等概念。
在C# lock關鍵字定義如下:
lock(expression) statement_block,其中expression代表你希望跟蹤的物件,通常是物件引用。
如果你想保護一個類的例項,一般地,你可以使用this;如果你想保護一個靜態變數(如互斥程式碼段在一個靜態方法內部),一般使用類名就可以了。
而statement_block就是互斥段的程式碼,這段程式碼在一個時刻內只可能被一個執行緒執行。
二、簡單例子
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Thread thread1 = new Thread(new ThreadStart(ThreadStart1));
thread1.Name = "Thread1";
Thread thread2 = new Thread(new ThreadStart(ThreadStart2));
thread2.Name = "Thread2";
Thread thread3 = new Thread(new ThreadStart(ThreadStart3));
thread3.Name = "Thread3";
thread1.Start();
thread2.Start();
thread3.Start();
Console.ReadKey();
}
static object _object = new object();
static void Done(int millisecondsTimeout)
{
Console.WriteLine(string.Format("{0} -> {1}.Start", DateTime.Now.ToString("HH:mm:ss"), Thread.CurrentThread.Name));
//下邊程式碼段同一時間只能由一個執行緒在執行
lock (_object)
{
Console.WriteLine(string.Format("{0} -> {1}進入鎖定區域.", DateTime.Now.ToString("HH:mm:ss"), Thread.CurrentThread.Name));
Thread.Sleep(millisecondsTimeout);
Console.WriteLine(string.Format("{0} -> {1}退出鎖定區域.", DateTime.Now.ToString("HH:mm:ss"), Thread.CurrentThread.Name));
}
}
static void ThreadStart1()
{
Done(5000);
}
static void ThreadStart2()
{
Done(3000);
}
static void ThreadStart2()
{
Done(1000);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
三、簡單解釋一下執行過程
先來看看執行過程,程式碼示例如下:
private static object ojb = new object();
lock(obj)
{
//鎖定執行的程式碼段
}
假設執行緒A先執行,執行緒B稍微慢一點。執行緒A執行到lock語句,判斷obj是否已申請了互斥鎖,判斷依據是逐個與已存在的鎖進行object.ReferenceEquals比較(此處未加證實),如果不存在,則申請一個新的互斥鎖,這時執行緒A進入lock裡面了。
這時假設執行緒B啟動了,而執行緒A還未執行完lock裡面的程式碼。執行緒B執行到lock語句,檢查到obj已經申請了互斥鎖,於是等待;直到執行緒A執行完畢,釋放互斥鎖,執行緒B才能申請新的互斥鎖並執行lock裡面的程式碼。
四、Lock的物件選擇問題
接下來說一些lock應該鎖定什麼物件。
1、為什麼不能lock值型別
比如lock(1)呢?lock本質上Monitor.Enter,Monitor.Enter會使值型別裝箱,每次lock的是裝箱後的物件。lock其實是類似編譯器的語法糖,因此編譯器直接限制住不能lock值型別。退一萬步說,就算能編譯器允許你lock(1),但是object.ReferenceEquals(1,1)始終返回false(因為每次裝箱後都是不同物件),也就是說每次都會判斷成未申請互斥鎖,這樣在同一時間,別的執行緒照樣能夠訪問裡面的程式碼,達不到同步的效果。同理lock((object)1)也不行。
2、Lock字串
那麼lock(“xxx”)字串呢?MSDN上的原話是:
鎖定字串尤其危險,因為字串被公共語言執行庫 (CLR)“暫留”。 這意味著整個程式中任何給定字串都只有一個例項,就是這同一個物件表示了所有執行的應用程式域的所有執行緒中的該文字。因此,只要在應用程式程序中的任何位置處具有相同內容的字串上放置了鎖,就將鎖定應用程式中該字串的所有例項。
3、MSDN推薦的Lock物件
通常,最好避免鎖定 public 型別或鎖定不受應用程式控制的物件例項。例如,如果該例項可以被公開訪問,則 lock(this) 可能會有問題,因為不受控制的程式碼也可能會鎖定該物件。這可能導致死鎖,即兩個或更多個執行緒等待釋放同一物件。出於同樣的原因,鎖定公共資料型別(相比於物件)也可能導致問題。
而且lock(this)只對當前物件有效,如果多個物件之間就達不到同步的效果。
而自定義類推薦用私有的只讀靜態物件,比如:
private static readonly object obj = new object();
為什麼要設定成只讀的呢?這時因為如果在lock程式碼段中改變obj的值,其它執行緒就暢通無阻了,因為互斥鎖的物件變了,object.ReferenceEquals必然返回false。
4、lock(typeof(Class))
與鎖定字串一樣,範圍太廣了。
五、特殊問題:Lock(this)等的詳細解釋
在以前程式設計中遇到lock問題總是使用lock(this)一鎖了之,出問題後翻看MSDN突然發現下面幾行字:通常,應避免鎖定 public 型別,否則例項將超出程式碼的控制範圍。常見的結構 lock (this)、lock (typeof (MyType)) 和 lock (“myLock”) 違反此準則:如果例項可以被公共訪問,將出現C# lock this問題。如果 MyType 可以被公共訪問,將出現 lock (typeof (MyType)) 問題。由於程序中使用同一字串的任何其他程式碼將共享同一個鎖,所以出現 lock(“myLock”) 問題。
來看看C# lock this問題:如果有一個類Class1,該類有一個方法用lock(this)來實現互斥:
publicvoidMethod2()
{
lock(this)
{
System.Windows.Forms.MessageBox.Show(“Method2End”);
}
}
如果在同一個Class1的例項中,該Method2能夠互斥的執行。但是如果是2個Class1的例項分別來執行Method2,是沒有互斥效果的。因為這裡的lock,只是對當前的例項物件進行了加鎖。
Lock(typeof(MyType))鎖定住的物件範圍更為廣泛,由於一個類的所有例項都只有一個型別物件(該物件是typeof的返回結果),鎖定它,就鎖定了該物件的所有例項,微軟現在建議,不要使用lock(typeof(MyType)),因為鎖定型別物件是個很緩慢的過程,並且類中的其他執行緒、甚至在同一個應用程式域中執行的其他程式都可以訪問該型別物件,因此,它們就有可能代替您鎖定型別物件,完全阻止您的執行,從而導致你自己的程式碼的掛起。
鎖住一個字串更為神奇,只要字串內容相同,就能引起程式掛起。原因是在.NET中,字串會被暫時存放,如果兩個變數的字串內容相同的話,.NET會把暫存的字串物件分配給該變數。所以如果有兩個地方都在使用lock(“my lock”)的話,它們實際鎖住的是同一個物件。到此,微軟給出了個lock的建議用法:鎖定一個私有的static 成員變數。
.NET在一些集合類中(比如ArrayList,HashTable,Queue,Stack)已經提供了一個供lock使用的物件SyncRoot,用Reflector工具查看了SyncRoot屬性的程式碼,在Array中,該屬性只有一句話:return this,這樣和lock array的當前例項是一樣的。ArrayList中的SyncRoot有所不同
get
{
if(this._syncRoot==null)
{
Interlocked.CompareExchange(refthis._syncRoot,newobject(),null);
}
returnthis._syncRoot;
其中Interlocked類是專門為多個執行緒共享的變數提供原子操作(如果你想鎖定的物件是基本資料型別,那麼請使用這個類),CompareExchange方法將當前syncRoot和null做比較,如果相等,就替換成new object(),這樣做是為了保證多個執行緒在使用syncRoot時是執行緒安全的。集合類中還有一個方法是和同步相關的:Synchronized,該方法返回一個對應的集合類的wrapper類,該類是執行緒安全的,因為他的大部分方法都用lock來進行了同步處理,比如Add方法:
publicoverridevoidAdd(objectkey,objectvalue)
{
lock(this._table.SyncRoot)
{
this._table.Add(key,value);
}
}
這裡要特別注意的是MSDN提到:從頭到尾對一個集合進行列舉本質上並不是一個執行緒安全的過程。即使一個集合已進行同步,其他執行緒仍可以修改該集合,這將導致列舉數引發異常。若要在列舉過程中保證執行緒安全,可以在整個列舉過程中鎖定集合:
QueuemyCollection=newQueue();
lock(myCollection.SyncRoot){
foreach(ObjectiteminmyCollection){
//Insertyourcodehere.
}
}
最後
注意:應避免鎖定 public 型別,否則例項將超出程式碼的控制範圍。常見的結構 lock (this)、lock (typeof (MyType)) 和 lock (“myLock”) 違反此準則:
1)如果例項可以被公共訪問,將出現 lock (this) 問題;
2)如果 MyType 可以被公共訪問,將出現 lock (typeof (MyType)) 問題;
3)由於程序中使用同一字串的任何其他程式碼將共享同一個鎖,所以出現 lock(“myLock”) 問題;
最佳做法是定義 private 物件來鎖定, 或 private static 物件變數來保護所有例項所共有的資料。