當涉及到多線程共享數據,需要數據同步的時候,就可以考慮使用線程鎖了。本篇體驗線程鎖的各種用法以及線程死鎖。主要包括:
※ 使用lock處理數據同步
※ 使用Monitor.Enter和Monitor.Exit處理數據同步
※ 使用Mutex處理進程間數據同步
※ 使用Semaphore處理數據同步
※ 線程死鎖
□ 使用lock處理數據同步
假設有一個類,主要用來計算該類2個字段的商,在計算商的方法之內讓被除數自減,即被除數有可能為零。使用lock語句塊保證每次只有一個線程進入該方法。
class ThreadSafe
{static readonly object o = new object();private static int _val1, _val2;public ThreadSafe(int val1, int val2){_val1 = val1;_val2 = val2;}public void Calculate(){lock (o)
{--_val2;if (_val2 != 0)
{Console.WriteLine(_val1/_val2);}else
{Console.WriteLine("_val2為零");
}}}}
○ new object()創建的對象實例,也被稱作同步對象
○ 同步對象必須是引用類型
○ 同步對象通常是私有的、靜態的
客戶端有一個靜態字段val2被ThreadSafe的2個實例方法共用。
class Program
{private static int val2 = 2;static void Main(string[] args){ThreadSafe ts1 = new ThreadSafe(2, val2);
ThreadSafe ts2 = new ThreadSafe(2, val2);
Thread[] threads = new Thread[2];
threads[0] = new Thread(ts1.Calculate);
threads[1] = new Thread(ts2.Calculate);
threads[0].Start();threads[1].Start();Console.ReadKey();}}
○ 雖然ThreadSafe的2個實例方法共用了客戶端靜態字段val2,因為有了lock的存在,保證了val2的數據同步
○ 使用lock出現異常,需要手動處理
□ 使用Monitor.Enter和Monitor.Exit處理數據同步
把上面的Calculate方法修改為:
public void Calculate(){Monitor.Enter(o);_val2--;try
{if (_val2 != 0)
{Console.WriteLine(_val1 / _val2);}else
{Console.WriteLine("被除數為零");
}}finally
{Monitor.Exit(o);}}
○ 能得到相同的結果。
○ lock其實是語法糖,其內部的實現邏輯就是Monitor.Enter和Monitor.Exit的實現邏輯
如果把Monitor.Exit注釋掉,會發生什么呢?
public void Calculate(){Monitor.Enter(o);_val2--;try
{if (_val2 != 0)
{Console.WriteLine(_val1 / _val2);}else
{Console.WriteLine("被除數為零");
}}finally
{//Monitor.Exit(o);
}}
不過,以上代碼還有一些不易察覺的、潛在的問題:如果在執行Monitor.Enter方法的時候出現異常,線程將拿不到鎖;如果在Monitor.Enter與try之間出現異常,由於無法執行try...catch語句塊,鎖得不到釋放。
為了解決以上問題, CLR 4.0給出了一個Monitor.Enter的重載方法。
public static void Enter (object obj, ref bool lockTaken);
現在,如果在執行Monitor.Enter方法的時候失敗,即沒有拿到鎖,lockTaken就為false,finally語句塊中無需釋放鎖;如果在Monitor.Enter之后出現異常,因為線程拿到了鎖,lockTaken就為true,最后在finally語句塊中釋放鎖。
所以,Calculate方法更健壯的寫法為:
public void Calculate(){bool lockTaken = false;_val2--;try
{Monitor.Enter(o, ref lockTaken);
if (_val2 != 0)
{Console.WriteLine(_val1 / _val2);}else
{Console.WriteLine("被除數為零");
}}finally
{if (lockTaken)
{Monitor.Exit(o);}}}
另外,Monitor還提供了多個靜態方法TryEnter的重載,可以指定在某個時間段內獲取鎖。
□ 使用Mutex處理進程間數據同步
Mutex的作用和lock相似,不過與lock不同的是:Mutex可以跨進程實施線程鎖。Mutex有2個重要的靜態方法:
○ WaitOne:阻止當前線程,如果收到當前實例的信號,則為true,否則為false
○ ReleaseMutex:用來釋放鎖,只有獲取鎖的線程才可以使用該方法,與lock一樣
Mutex一個經典應用就是:同一時間只能允許一個實例出現。
class Program
{static Mutex mutex = new Mutex(true,"darren.mutex");static void Main(string[] args){if (!mutex.WaitOne(2000))//如果找到互拆體,即有另外一個相同的實例在運行着{Console.WriteLine("另外一個實例已經在運行着了~~");
Console.ReadLine();}else//如果沒有發現互拆體{try
{RunAnother();}finally
{mutex.ReleaseMutex();}}}static void RunAnother(){Console.WriteLine("我是模擬另外一個實例正在運行着~~不過可以按回車鍵退出");
Console.ReadLine();}}
以上是分別2次雙擊應用程序后的結果。
□ 使用Semaphore處理數據同步
Semaphore可以被形象地看成是一個舞池,比如該舞池最多能容納100人,超過100,都要在舞池外邊排隊等候進入。如果舞池中有一個人離開,在外面等候隊列中排在最前面的那個人就可以進入舞池。
如果舞池的容量是1,這時候Semaphore就和Mutex與lock很像了。不過,與Mutex和lock不同的是,任何線程都可以釋放Semaphore。
class Program
{static Semaphore _semaphore = new Semaphore(3,3);static void Main(string[] args){Console.WriteLine("ladies and gentleman,舞會開始了~~");
for (int i = 1; i <= 5; i++){new Thread(IWannaDance).Start(i);
}}static void IWannaDance(object id){Console.WriteLine(id + "想跳舞");
_semaphore.WaitOne();Console.WriteLine(id + "進了");
Thread.Sleep(3000);Console.WriteLine(id + "准備離開舞池了");
_semaphore.Release();}}
□ 線程死鎖
有2個線程:線程1和線程2。有2個資源,資源1和資源2。線程1已經拿到了資源1的鎖,還想拿資源2的鎖,線程2已經拿到了資源2的鎖,同時還想拿資源1的鎖。線程1和線程2都沒有放棄自己的鎖,還同時想要另外的鎖,這就形成線程死鎖。就像2個小孩,手上都有自己的玩具,卻還想要對方的玩具,誰也不肯讓誰。
舉一個銀行轉賬的例子來呈現線程死鎖。
首先是銀行賬戶,提供了存款和取款的方法。
public class Account{private double _balance;private int _id;public Account(int id, double balance){this._id = id;
this._balance = balance;
}public int ID{get { return _id; }}//取款
public void Withdraw(double amount){_balance -= amount;}//存款
public void Deposit(double amount){_balance += amount;}}
其次是用來轉賬的一個管理類。
public class AccountManager{private Account _fromAccount;
private Account _toAccount;
private double _amountToTransfer;public AccountManager(Account fromAccount, Account toAccount, double amount){this._fromAccount = fromAccount;
this._toAccount = toAccount;
this._amountToTransfer = _amountToTransfer;
}//轉賬
public void Transfer(){Console.WriteLine(Thread.CurrentThread.Name + "正在" + _fromAccount.ID.ToString() + "獲取鎖");lock (_fromAccount)
{Console.WriteLine(Thread.CurrentThread.Name + "已經" + _fromAccount.ID.ToString() + "獲取到鎖");Console.WriteLine(Thread.CurrentThread.Name + "被阻塞1秒");
//模擬處理時間
Thread.Sleep(1000);Console.WriteLine(Thread.CurrentThread.Name + "醒了,想想獲取" + _toAccount.ID.ToString() + "的鎖");lock (_toAccount)
{Console.WriteLine("如果造成線程死鎖,這里的代碼就不執行了~~");
_fromAccount.Withdraw(_amountToTransfer);_toAccount.Deposit(_amountToTransfer);}}}}
○ 使用了2個lock,稱為"嵌套鎖",當一個方法中調用另外的方法,通常使用"嵌套鎖"
○ 第1個lock下的Thread.Sleep(1000)讓線程阻塞1秒,好讓另一個線程進來
○ 把"正在獲取XX鎖","已經獲取到XX鎖"......等狀態,打印到控制台上
客戶端開2個線程,一個線程賬戶A向賬戶B轉賬,另一個線程賬戶B向賬戶A轉賬。
class Program
{static void Main(string[] args){Console.WriteLine("准備轉賬了");
Account accountA = new Account(1, 5000);
Account accountB = new Account(2, 3000);
AccountManager accountManagerA = new AccountManager(accountA, accountB, 1000);
Thread threadA = new Thread(accountManagerA.Transfer);
threadA.Name = "線程A";
AccountManager accountManagerB = new AccountManager(accountB, accountA, 2000);
Thread threadB = new Thread(accountManagerB.Transfer);
threadB.Name = "線程B";
threadA.Start();threadB.Start();threadA.Join();threadB.Join();Console.WriteLine("轉賬完成");
}}
正如死鎖的定義:線程A獲取鎖1,線程2獲取鎖2,線程A想獲取鎖2,同時線程B想獲取鎖1。結果:線程死鎖。
○ 獲取鎖和釋放鎖的過程是相當快的,大概在幾十納秒的數量級
○ 線程鎖能解決並發問題,但如果持有鎖的時間過長,會增加線程死鎖的可能
總結:
○ 同一進程內,在同一時間,只有一個線程獲取鎖,占用一個資源或一段代碼,使用lock或Monitor.Enter/Monitor.Exit
○ 同一進程或不同進程內,在同一時間,只有一個線程獲取鎖,占用一個資源或一段代碼,使用Mutex
○ 同一進程或不同進程內,在同一時間,規定有限的線程占有一個資源或一段代碼,使用Semaphore
○ 使用線程鎖的時候要注意造成線程死鎖,當線程持有鎖的時間過長,容易造成線程死鎖
線程系列包括: