當涉及到多線程共享數據,需要數據同步的時候,就可以考慮使用線程鎖了。本篇體驗線程鎖的各種用法以及線程死鎖。主要包括:
※ 使用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.Exit,會捕捉不到異常。
不過,以上代碼還有一些不易察覺的、潛在的問題:如果在執行Monitor.Enter方法的時候出現異常,線程將拿不到鎖;如果在Monitor.Enter與try之間出現異常,由於無法執行try...catch語句塊,鎖得不到釋放。
為了解決以上問題, CLR 4.0給出了一個Monitor.Enter的重載方法。
public static void Enter (object obj, ref bool lockTaken);
所以,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();}}
可見,舞池最多可容納3人,超過3人都得排隊。
□ 線程死鎖
有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
○ 使用線程鎖的時候要注意造成線程死鎖,當線程持有鎖的時間過長,容易造成線程死鎖
線程系列包括:
這麼專業的問題還是不要在這問了,白費時間和精力!本人的多線程死鎖還一直是個難題,再加上socket通訊的阻塞與非阻塞,非常不好辦。
網上也就解決點常識性的,別的還是需要閉門造車的精神多做研究吧
1、 Event 用事件(Event)來同步線程是最具彈性的了。一個事件有兩種狀態:激發狀態和未激發狀態。也稱有信號狀態和無信號狀態。事件又分兩種類型:手動重置事件和自動重置事件。手動重置事件被設置為激發狀態後,會喚醒所有等待的線程,而且一直保持為激發狀態,直到程序重新把它設置為未激發狀態。自動重置事件被設置為激發狀態後,會喚醒“一個”等待中的線程,然後自動恢復為未激發狀態。所以用自動重置事件來同步兩個線程比較理想。MFC中對應的類為CEvent.。CEvent的構造函數默認創建一個自動重置的事件,而且處於未激發狀態。共有三個函數來改變事件的狀態:SetEvent,ResetEvent和PulseEvent。用事件來同步線程是一種比較理想的做法,但在實際的使用過程中要注意的是,對自動重置事件調用SetEvent和PulseEvent有可能會引起死鎖,必須小心。 多線程同步-event 在所有的內核對象中,事件內核對象是個最基本的。它包含一個使用計數(與所有內核對象一樣),一個BOOL值(用於指明該事件是個自動重置的事件還是一個人工重置的事件),還有一個BOOL值(用於指明該事件處於已通知狀態還是未通知狀態)。事件能夠通知一個線程的操作已經完成。有兩種類型的事件對象。一種是人工重置事件,另一種是自動重置事件。他們不同的地方在於:當人工重置的事件得到通知時,等待該事件的所有線程均變為可調度線程。當一個自動重置的事件得到通知時,等待該事件的線程中只有一個線程變為可調度線程。 當一個線程執行初始化操作,然後通知另一個線程執行剩余的操作時,事件使用得最頻繁。在這種情況下,事件初始化為未通知狀態,然後,當該線程完成它的初始化操作後,它就將事件設置為已通知狀態,而一直在等待該事件的另一個線程在事件已經被通知後,就變成可調度線程。 當這個進程啟動時,它創建一個人工重置的未通知狀態的事件,並且將句柄保存在一個全局變量中。這使得該進程中的其他線程能夠非常容易地訪問同一個事件對象。程序一開始創建了三個線程,這些線程在初始化後就被掛起,等待事件。這些線程要等待文件的內容讀入內存,然後每個線程都會訪問這段文件內容。一個線程進行單詞計數,另一個線程運行拼寫檢查,第三個線程運行語法檢查。這3個線程函數的代碼的開始部分都相同,每個函數都調用WaitForSingleObject.,這將使線程暫停運行,直到文件的內容由主線程讀入內存為止。一旦主線程將數據准備好,它就調用SetEvent,給事件發出通知信號。這時,系統就使所有這3個輔助線程進入可調度狀態,它們都獲得了C P U時間,並且可以訪問內存塊。這3個線程都必須以只讀方式訪問內存,否則會出現內存錯誤。這就是所有3個線程能夠同時運行的唯一原因。如果計算機上配有三個以上CPU,理論上這個3個線程能夠真正地同時運行,從而可以在很短的時間內完成大量的操作 如果你使用自動重置的事件而不是人工重置的事件,那麼應用程序的行為特性就有很大的差別。當主線程調用S e t E v e n t之後,系統只允許一個輔助線程變成可調度狀態。同樣,也無法保證系統將使哪個線程變為可調度狀態。其余兩個輔助線程將繼續等待。已經變為可調度狀態的線程擁有對內存塊的獨占訪問權。 讓我們重新編寫線程的函數,使得每個函數在返回前調用S e t E v e n t函數(就像Wi n M a i n函數所做的那樣)。 當主線程將文件內容讀入內存後,它就調用SetEvent函數,這樣操作西永就會使這三個在等待的線程中的一個成為可調度線程。我們不知道系統將首先......余下全文>>