1. 程式人生 > 實用技巧 >c#初學-多執行緒中lock用法的經典例項 (轉)

c#初學-多執行緒中lock用法的經典例項 (轉)

一、Lock定義

lock 關鍵字可以用來確保程式碼塊完成執行,而不會被其他執行緒中斷。它可以把一段程式碼定義為互斥段(critical section),互斥段在一個時刻內只允許一個執行緒進入執行,而其他執行緒必須等待。這是通過在程式碼塊執行期間為給定物件獲取互斥鎖來實現的。

在多執行緒中,每個執行緒都有自己的資源,但是程式碼區是共享的,即每個執行緒都可以執行相同的函式。這可能帶來的問題就是幾個執行緒同時執行一個函式,導致資料的混亂,產生不可預料的結果,因此我們必須避免這種情況的發生。

而在.NET中最好了解一下程序、應用域和執行緒的概念,因為Lock是針對執行緒一級的,而在.NET中應用域是否會對Lock起隔離作用,我的猜想是,即不在同一應用域中的執行緒無法通過Lock來中斷;另外也最好能瞭解一下資料段、程式碼段、堆、棧等概念。

在C# lock關鍵字定義如下:

lock(expression) statement_block,其中expression代表你希望跟蹤的物件,通常是物件引用。

如果你想保護一個類的例項,一般地,你可以使用this;如果你想保護一個靜態變數(如互斥程式碼段在一個靜態方法內部),一般使用類名就可以了。

而statement_block就是互斥段的程式碼,這段程式碼在一個時刻內只可能被一個執行緒執行。

二、簡單解釋一下執行過程

先來看看執行過程,程式碼示例如下:

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)來實現互斥:

  1. publicvoidMethod2()
  2. {
  3. lock(this)
  4. {
  5. System.Windows.Forms.MessageBox.Show("Method2End");
  6. }
  7. }

如果在同一個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有所不同

  1. get
  2. {
  3. if(this._syncRoot==null)
  4. {
  5. Interlocked.CompareExchange(refthis._syncRoot,newobject(),null);
  6. }
  7. returnthis._syncRoot;

其中Interlocked類是專門為多個執行緒共享的變數提供原子操作(如果你想鎖定的物件是基本資料型別,那麼請使用這個類),CompareExchange方法將當前syncRoot和null做比較,如果相等,就替換成new object(),這樣做是為了保證多個執行緒在使用syncRoot時是執行緒安全的。集合類中還有一個方法是和同步相關的:Synchronized,該方法返回一個對應的集合類的wrapper類,該類是執行緒安全的,因為他的大部分方法都用lock來進行了同步處理,比如Add方法:

  1. publicoverridevoidAdd(objectkey,objectvalue)
  2. {
  3. lock(this._table.SyncRoot)
  4. {
  5. this._table.Add(key,value);
  6. }
  7. }

這裡要特別注意的是MSDN提到:從頭到尾對一個集合進行列舉本質上並不是一個執行緒安全的過程。即使一個集合已進行同步,其他執行緒仍可以修改該集合,這將導致列舉數引發異常。若要在列舉過程中保證執行緒安全,可以在整個列舉過程中鎖定集合:

  1. QueuemyCollection=newQueue();
  2. lock(myCollection.SyncRoot){
  3. foreach(ObjectiteminmyCollection){
  4. //Insertyourcodehere.
  5. }
  6. }

最後

注意:應避免鎖定 public 型別,否則例項將超出程式碼的控制範圍。常見的結構 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 違反此準則: 1)如果例項可以被公共訪問,將出現 lock (this) 問題; 2)如果 MyType 可以被公共訪問,將出現 lock (typeof (MyType)) 問題; 3)由於程序中使用同一字串的任何其他程式碼將共享同一個鎖,所以出現 lock("myLock") 問題; 最佳做法是定義 private 物件來鎖定, 或 private static 物件變數來保護所有例項所共有的資料。

六、參考資料

由於參考的資料都儲存在本地,只能先列出標題,無法提供原文地址,深表歉意!

1)描述C#多執行緒中Lock關鍵字

2)解決C# lock this問題

3)基於C#中的lock關鍵字的總結

4)C# lock關鍵字

原文連結:http://www.soaspx.com/dotnet/csharp/csharp_20120104_8511.html


關於lock網上說法一大堆,但是關於實際用法的例項還是比較多的,但是多而不精,沒說的很透徹,但是這個例子是對多執行緒中使用lock關鍵字是一個相當好的例項。很鬱悶現在網上找到像樣的文章都沒有了,抄來抄去!!又不註明網址,還當自己的是原創!找個例子都找不到,還不如自己來~

下面引入lock關鍵字的理論:

在應用程式中使用多個執行緒的一個好處是每個執行緒都可以非同步執行。對於 Windows 應用程式,耗時的任務可以在後臺執行,而使應用程式視窗和控制元件保持響應。對於伺服器應用程式,多執行緒處理提供了用不同執行緒處理每個傳入請求的能力。否則,在完全滿足前一個請求之前,將無法處理每個新請求。 然而,執行緒的非同步特性意味著必須協調對資源(如檔案控制代碼、網路連線和記憶體)的訪問。否則,兩個或更多的執行緒可能在同一時間訪問相同的資源,而每個執行緒都不知道其他執行緒的操作。結果將產生不可預知的資料損壞。 對於整數資料型別的簡單操作,可以用 Interlocked 類的成員來實現執行緒同步。對於其他所有資料型別和非執行緒安全的資源,只有使用本主題中的結構才能安全地執行多執行緒處理。

lock 關鍵字可以用來確保程式碼塊完成執行,而不會被其他執行緒中斷。這是通過在程式碼塊執行期間為給定物件獲取互斥鎖來實現的。

提供給 lock 關鍵字的引數必須為基於引用型別的物件,該物件用來定義鎖的範圍。在上例中,鎖的範圍限定為此函式,因為函式外不存在任何對該物件的引用。如果確實存在此類引用,鎖的範圍將擴充套件到該物件。嚴格地說,提供給 lock 的物件只是用來唯一地標識由多個執行緒共享的資源,所以它可以是任意類例項。然而,實際上,此物件通常表示需要進行執行緒同步的資源。例如,如果一個容器物件將被多個執行緒使用,則可以將該容器傳遞給 lock,而 lock 後面的同步程式碼塊將訪問該容器。只要其他執行緒在訪問該容器前先鎖定該容器,則對該物件的訪問將是安全同步的。

通常,最好避免鎖定 public 型別或鎖定不受應用程式控制的物件例項。例如,如果該例項可以被公開訪問,則 lock(this) 可能會有問題,因為不受控制的程式碼也可能會鎖定該物件。這可能導致死鎖,即兩個或更多個執行緒等待釋放同一物件。出於同樣的原因,鎖定公共資料型別(相比於物件)也可能導致問題。鎖定字串尤其危險,因為字串被公共語言執行庫 (CLR)“暫留”。 這意味著整個程式中任何給定字串都只有一個例項,就是這同一個物件表示了所有執行的應用程式域的所有執行緒中的該文字。因此,只要在應用程式程序中的任何位置處具有相同內容的字串上放置了鎖,就將鎖定應用程式中該字串的所有例項。因此,最好鎖定不會被暫留的私有或受保護成員。某些類提供專門用於鎖定的成員。例如,Array 型別提供 SyncRoot。許多集合型別也提供 SyncRoot。

下面有註釋,有一定執行緒基礎都是可以看懂的。【VS2008 .NET3.5】

/*
該例項是一個執行緒中lock用法的經典例項,使得到的balance不會為負數
同時初始化十個執行緒,啟動十個,但由於加鎖,能夠啟動呼叫WithDraw方法的可能只能是其中幾個
作者:http://hi.baidu.com/jiang_yy_jiang
*/
using System;

namespace ThreadTest29
{
class Account
{
private Object thisLock = new object();
int balance;
Random r = new Random();

public Account(int initial)
{
balance = initial;
}

int WithDraw(int amount)
{
if (balance < 0)
{
throw new Exception("負的Balance.");
}
//確保只有一個執行緒使用資源,一個進入臨界狀態,使用物件互斥鎖,10個啟動了的執行緒不能全部執行該方法
lock (thisLock)
{
if (balance >= amount)
{
Console.WriteLine("----------------------------:" + System.Threading.Thread.CurrentThread.Name + "---------------");

Console.WriteLine("呼叫Withdrawal之前的Balance:" + balance);
Console.WriteLine("把Amount輸入 Withdrawal :-" + amount);
//如果沒有加物件互斥鎖,則可能10個執行緒都執行下面的減法,加減法所耗時間片段非常小,可能多個執行緒同時執行,出現負數。
balance = balance - amount;
Console.WriteLine("呼叫Withdrawal之後的Balance :" + balance);
return amount;
}
else
{
//最終結果
return 0;
}
}
}
public void DoTransactions()
{
for (int i = 0; i < 100; i++)
{
//生成balance的被減數amount的隨機數
WithDraw(r.Next(1, 100));
}
}
}

class Test
{
static void Main(string[] args)
{
//初始化10個執行緒
System.Threading.Thread[] threads = new System.Threading.Thread[10];
//把balance初始化設定為1000
Account acc = new Account(1000);
for (int i = 0; i < 10; i++)
{
System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ThreadStart(acc.DoTransactions));
threads[i] = t;
threads[i].Name = "Thread" + i.ToString();
}
for (int i = 0; i < 10; i++)
{
threads[i].Start();
}
Console.ReadKey();
}
}
}
https://www.cnblogs.com/promise-7/articles/2354077.html