C#线程 ---- 线程同步详解
線程同步
說明:接上一篇,注意分享線程同步的必要性和線程同步的方法。
測試代碼下載:https://github.com/EkeSu/C-Thread-synchronization-C-.git
一、什么是線程同步:
在同一時間只允許一個線程訪問資源的情況稱為線程同步。
二、為什么需要線程同步:
- 避免競爭條件;
- 確保線程安全;(如果兩個線程同時訪問一個資源并對那個資源做修改,就不安全了)
現(xiàn)在的計算機(jī)變得越來越多核,每一個CPU可以獨立工作,但是對于內(nèi)存和外部資源、數(shù)據(jù)庫的訪問卻可能因為不同線程的訪問使數(shù)據(jù)產(chǎn)生異常,常見的例子就是銀行的轉(zhuǎn)賬的例子不再贅述。
?
三、線程同步的方法:
- 同步代碼中重要的部分;
- 使對象不可改變;
- 使用線程安全包裝器;
注意:局部變量、方法參數(shù)和返回值是放在堆棧中的,本身是線程安全的。
四、線程不安全的演示:
背景:在數(shù)據(jù)庫的user_blance表插入兩條數(shù)據(jù),兩人的balance值都為1000.00,整個user_balance表的balance總值為2000.00
static string connectionStr = "Server=127.0.0.1;Port=3306;Stmt=;Database=exe_dev; User=root;Password=123456";public static void UnSafeThread() {Thread ThreadOne = new Thread(new ThreadStart(DrawMoney));ThreadOne.Name = "A001";Thread ThreadTwo = new Thread(new ThreadStart(DrawMoney));ThreadTwo.Name = "A002";ThreadOne.Start();ThreadTwo.Start();}private static void DoDrawMoney(){Random random = new Random();int money = random.Next(100);string userId = Thread.CurrentThread.Name;string selectSql = "select balance from user_balance where user_id=@UserId";string updateSql = "update user_balance set balance=@Balance+@Money where user_id=@UserId";string updateSql2 = "update user_balance set balance=@Balance-@Money where user_id<>@UserId";using (MySqlConnection conn= MySqlConnectionHelper.OpenConnection(connectionStr)){var balance = conn.ExecuteScalar(selectSql, new { UserId = userId });if (balance != null){conn.Execute(updateSql, new { Money = money, Balance=balance, UserId = userId });conn.Execute(updateSql2, new { Money = money, Balance = balance, UserId = userId });}}}private static void DrawMoney() {for (int i = 0; i < 100; i++){DoDrawMoney();}}運(yùn)行結(jié)果:
程序中有三條線程在跑:兩條支線程,一條主線程,主線程負(fù)責(zé)統(tǒng)計錢的總數(shù),兩條支線程模擬兩個人賺錢,賺過來賺過去,哈哈哈,依據(jù)查詢成果可以看到,錢的總數(shù)原本是2000.00,但是之后開始減少。當(dāng)然上面的異常也可以通過加事務(wù)解決,或者改變sql的實現(xiàn)方式balance=balance+money,不過這個不是我們討論的重點,不展開。
?五、線程同步:
1、MethodImplAttribute:同步方法
- 對象:方法。
- 使用方式:放在方法上,作為方法的屬性
MethodImplAttribute是一個屬性,它用來告訴CLR方法是如何實現(xiàn)的,MethodImplAttribute的一個構(gòu)造函數(shù)把MethodImplOptions的枚舉值作為參數(shù),MethodImplOptions的枚舉值Synchronized告訴CLR,這個方法該一次性只能在一個線程上執(zhí)行。 靜態(tài)方法在類型上鎖定,而實例方法在實例上鎖定。 只有一個線程可在任意實例函數(shù)中執(zhí)行,且只有一個線程可在任意類的靜態(tài)函數(shù)中執(zhí)行。
使用方式:在需要同步的方法上添加屬性
?
?2、SynchronizationAttribute 同步方法----同步上下文:
- 對象:非靜態(tài)類。
- 使用方式:放在非靜態(tài)類上,作為非靜態(tài)類的屬性,同時非靜態(tài)類需要繼承【ContextBoundObject】
?
代碼演示(其余部分和不安全的演示代碼完全一樣):
[Synchronization]class ThreadTestForSynchronization : ContextBoundObject{上下文是一組屬性或使用規(guī)則,這組屬性或使用規(guī)則對執(zhí)行時的相關(guān)對象都是通用的。我的理解是這些組成了程序運(yùn)行的環(huán)境,我們使用【Synchronization】來在上下文中添加規(guī)則【ThreadTestForSynchronization】類是需要線程同步的,所以程序在運(yùn)行的時候就是線程同步的。
注意:當(dāng)前我使用的環(huán)境是VS2017, .Net Framework4.61,C#版本應(yīng)該是6.0,SynchronizationAttribute屬性和更早版本是發(fā)生了很大變化,更早版本的構(gòu)造函數(shù)需要填入一個枚舉值。
?3、使用Monitor同步----同步重要代碼塊:
- 對象:代碼塊;
- 使用方式:使用Monitor.Enter(object obj),Monitor.Exit(object obj);這兩個是配套使用的,兩個方法之間是需要同步的重要代碼塊,Enter放入的對象和Exit釋放的對象應(yīng)該一致。Enter使對象獲得鎖,Exit釋放鎖。
- 注意事項:這兩個方法的參數(shù)是object,所以不能鎖住值類型參數(shù),因為會導(dǎo)致發(fā)生裝箱,裝箱之后的數(shù)值是一樣的,但是已經(jīng)不是同一個東西了。
代碼演示(下面的例子鎖住number會報錯,因為發(fā)生裝箱):
private int number;public void MonitorThread(){Thread ThreadOne = new Thread(new ThreadStart(PrintNumber));ThreadOne.Name = "梁山伯";Thread ThreadTwo = new Thread(new ThreadStart(PrintNumber));ThreadTwo.Name = "祝英臺";ThreadOne.Start();ThreadTwo.Start();}private void PrintNumber(){Console.WriteLine(string.Format("Thread {0} enter Method:",Thread.CurrentThread.Name));Monitor.Enter(this);for (int i = 0; i < 10; i++){Console.WriteLine(string.Format("Thread {0} increase number value:{1}", Thread.CurrentThread.Name, number++));}Monitor.Exit(this);Console.WriteLine(string.Format("Thread {0} exit Method:", Thread.CurrentThread.Name));}?輸出結(jié)果會是很工整的,占篇幅就不貼出來了。
Monitor.Enter()方法是去爭取鎖,還有另外一個方法是可以去爭取鎖并且進(jìn)行等待的,就是TryEnter()方法,該方法可以有一個返回值,返回是否成功獲得鎖。同時該方法有三個重載:
bool TryEnter(object obj);? ?---- 有返回值不等待
bool TryEnter(object obj, int millisecondsTimeout);? ----- 有返回值且會等待鎖
void TryEnter(object obj, TimeSpan timeout, ref bool lockTaken);? ?----- 等待鎖,成功則返回true到lockTaken.
下面看一下被改編的代碼:
演示結(jié)果:
Thread 梁山伯 enter Method: Thread 梁山伯 Get Lock True Thread 祝英臺 enter Method: Thread 梁山伯 increase number value:0 Thread 梁山伯 increase number value:1 Thread 梁山伯 increase number value:2 Thread 梁山伯 increase number value:3 Thread 梁山伯 increase number value:4 Thread 梁山伯 increase number value:5 Thread 梁山伯 increase number value:6 Thread 梁山伯 increase number value:7 Thread 梁山伯 increase number value:8 Thread 祝英臺 Get Lock False Thread 梁山伯 increase number value:9 Thread 梁山伯 exit Method: Thread 祝英臺 increase number value:10 Thread 祝英臺 increase number value:11 Thread 祝英臺 increase number value:12 Thread 祝英臺 increase number value:13 Thread 祝英臺 increase number value:14 Thread 祝英臺 increase number value:15 Thread 祝英臺 increase number value:16 Thread 祝英臺 increase number value:17 Thread 祝英臺 increase number value:18 Thread 祝英臺 increase number value:19 Thread 祝英臺 exit Method:分析:線程梁山伯先進(jìn)入方法PrintNumber,它先獲得鎖,并且去執(zhí)行函數(shù),線程祝英臺等待的時間為1000毫秒,線程祝英臺每次循環(huán)都要睡眠100毫秒,循環(huán)10次需要睡眠1000毫秒,所以線程祝英臺等待1000毫秒是無法獲得鎖的,那么線程祝英臺什么時候執(zhí)行呢,就是趁著線程梁山伯睡眠的時候執(zhí)行,所以等待1000毫秒之后,線程祝英臺趁梁山伯睡眠的時候執(zhí)行了。如果我們把祝英臺等待鎖的時間延長到2000毫秒,那么她就可以等待鎖成功。需要注意的是,等待時間是1000毫秒的時候祝英臺是沒有獲得鎖的,所以不能執(zhí)行Monitor.Exit(this)操作,沒有獲得鎖自然無法獲得鎖,所以需要加一個 if 的判斷。
筆者理解:線程梁山伯睡眠的時候祝英臺開始執(zhí)行了,但是并沒有獲得鎖,就進(jìn)入了被保護(hù)的代碼塊,說明,線程睡眠的時候是會去釋放鎖,睡眠之后是或重新獲得鎖的,這里面應(yīng)該有復(fù)雜的機(jī)制處理,值得研究。
?
4、使用Monitor同步重要代碼塊,并使用Wait,Pulse方法做線程間的交互-----等待和發(fā)出脈沖機(jī)制:
- 對象:代碼塊;
- 使用方式:Wait,Pulse在Enter和Exit方法之間調(diào)用,Wait方法:當(dāng)在對象上調(diào)用Wait方法時,正在訪問被Monitor對象的線程會釋放鎖并將進(jìn)入等待狀態(tài)(包括調(diào)用它的線程自己);Pulse方法:發(fā)出一個信號通知正在等待的線程可以繼續(xù)執(zhí)行了,即等待的線程可以重新競爭獲得鎖繼續(xù)執(zhí)行。
- 個人對獲得鎖的理解:鎖就是權(quán)力,有鎖的人就有權(quán)力執(zhí)行程序
演示程序:
?
class ThreadTestForMonitorWait{private int result;private LockData lockData;public ThreadTestForMonitorWait(){this.lockData = new LockData();}public void MonitorWaitThread(){Thread ThreadOne = new Thread(new ThreadStart(WaitFirstThread));ThreadOne.Name = "WaitFirstThread";Thread ThreadTwo = new Thread(new ThreadStart(PulseFirstThread));ThreadTwo.Name = "PulseFirstThread";ThreadOne.Start();ThreadTwo.Start();}private void WaitFirstThread(){Monitor.Enter(lockData);Console.WriteLine(string.Format("Thread {0} enter MonitorWaitThread",Thread.CurrentThread.Name));for (int i = 0; i < 5; i++){Monitor.Wait(lockData);Console.WriteLine(string.Format("Thread {0} increase number value {1}", Thread.CurrentThread.Name, result++));Monitor.Pulse(lockData);}Console.WriteLine(string.Format("Thread {0} exit MonitorWaitThread", Thread.CurrentThread.Name));Monitor.Exit(lockData);}private void PulseFirstThread(){Monitor.Enter(lockData);Console.WriteLine(string.Format("Thread {0} enter MonitorWaitThread", Thread.CurrentThread.Name));for (int i = 0; i < 5; i++){Monitor.Pulse(lockData);Console.WriteLine(string.Format("Thread {0} increase number value {1}", Thread.CurrentThread.Name, result++));Monitor.Wait(lockData);}Console.WriteLine(string.Format("Thread {0} exit MonitorWaitThread", Thread.CurrentThread.Name));Monitor.Exit(lockData);}}public class LockData { }運(yùn)行結(jié)果:
Thread WaitFirstThread enter MonitorWaitThread Thread PulseFirstThread enter MonitorWaitThread Thread PulseFirstThread increase number value 0 Thread WaitFirstThread increase number value 1 Thread PulseFirstThread increase number value 2 Thread WaitFirstThread increase number value 3 Thread PulseFirstThread increase number value 4 Thread WaitFirstThread increase number value 5 Thread PulseFirstThread increase number value 6 Thread WaitFirstThread increase number value 7 Thread PulseFirstThread increase number value 8 Thread WaitFirstThread increase number value 9 Thread WaitFirstThread exit MonitorWaitThread Thread PulseFirstThread exit MonitorWaitThread可見:運(yùn)行結(jié)果是很工整的,在上面的程序中,WaitFirstThread?方法先被調(diào)用, WaitFirstThread 進(jìn)入循環(huán)會調(diào)用Wait方法,這個時候他會失去鎖,因此無法繼續(xù)執(zhí)行程序,而后方法?PulseFirstThread 被調(diào)用,它所在的線程獲得鎖,不管三七二十一先來發(fā)出一個脈沖通知正在被鎖定的線程:兄弟你可以繼續(xù)競爭獲得鎖了,在一次循環(huán)的末尾它又調(diào)用了Wait方法,使自己失去鎖,其他的線程可以競爭得到鎖,所以接著?WaitFirstThread 所在的線程就獲得了鎖,整個過程就變成兩個線程之間鎖給來給去,非常恩愛。
問題:其實這個程序是有問題的,就是必須要線程WaitFirstThread先執(zhí)行,先釋放鎖,否則若是PulseFirstThread先執(zhí)行,它先通知其他線程可以競爭鎖了,之后執(zhí)行一次循環(huán)把自己的鎖釋放掉,而線程競爭得到鎖之后干的第一件事就是釋放鎖,這樣就大家都沒有鎖了,死鎖就產(chǎn)生了。有興趣可以試試看。
5.使用lock關(guān)鍵字同步重要代碼塊 ---- 一塊封裝了Monitor的語法糖。
直接代碼演示(改一下Monitor的PrintNumber方法的寫法就行):
private void PrintNumber(){Console.WriteLine(string.Format("Thread {0} enter Method:", Thread.CurrentThread.Name));lock (this){for (int i = 0; i < 10; i++){Console.WriteLine(string.Format("Thread {0} increase number value:{1}", Thread.CurrentThread.Name, number++));}}Console.WriteLine(string.Format("Thread {0} exit Method:", Thread.CurrentThread.Name));}筆者覺得,比較好用的自然是lock,其實我?guī)缀鯖]有見過用Monitor的,但是一定要學(xué)會使用Monitor。
?6.使用ReaderWriterLock鎖定文件:
我們一定都遇到過要寫入一個文件返回該文件已經(jīng)被其他程序鎖定的錯誤,這個就是無法獲得寫鎖,增加這個鎖定可以防止這樣的錯誤發(fā)生,使獲得文件的寫鎖更有序。
對象:文件;
使用方式:
AcquireWriterLock(100); 嘗試獲取文件寫鎖;ReleaseWriterLock(); 釋放文件寫鎖。 ?
代碼演示(運(yùn)行時可以把下面綠色的代碼注釋掉以做比較會比較直觀):
private void WriterFileLock(){try{ rwl.AcquireWriterLock(100);using (StreamWriter writer = new StreamWriter("@ReadWriterLokTest.text")){for (int i = 0; i < 1000; i++){writer.WriteLine(i);}}Console.WriteLine("File Writer Finish");}catch (Exception ex){Console.WriteLine(ex.Message);}finally{ rwl.ReleaseWriterLock();}}?
7、其他同步方式:
?(圖片來源:C#線程參考手冊-清華大學(xué)出版社)
?7.1 使用ManualResetEvent 同步
先看代碼演示:
static ManualResetEvent manualResetEvent = new ManualResetEvent(false);static Stopwatch stopwatch = new Stopwatch();public static void ManualResetEventThread(){stopwatch.Start();var success = manualResetEvent.WaitOne(1000, false);Console.WriteLine(Thread.CurrentThread.GetHashCode()+"獲取信號:" + success + ",時間:"+ stopwatch.Elapsed);manualResetEvent.Set();success = manualResetEvent.WaitOne(1000, false);Console.WriteLine(Thread.CurrentThread.GetHashCode() + "獲取信號:" + success + ",時間:" + stopwatch.Elapsed);}輸出結(jié)果:
1獲取信號:False,時間:00:00:01.0425731 1獲取信號:True,時間:00:00:01.0655502ManualResetEvent 有兩個方法:
Set():使?fàn)顟B(tài)變成有信號;
ReSet():使?fàn)顟B(tài)變成無信號。
其實ManualResetEvent 只是很簡單的在不同線程同步了一個信號而已,并不會阻礙線程的向下繼續(xù)執(zhí)行,而WaitOne方法可以設(shè)置等待線程獲取信號的時間,這個方法可以延緩線程的執(zhí)行(一般不要這么玩)。
在使用上,可以根據(jù)WaitOne獲取的狀態(tài)來判斷當(dāng)前線程要不要繼續(xù)執(zhí)行。
另外需要介紹一下WaitAll和WaitAny的方法,首先是WaitAll:
我們先定義了三個方法:
static ManualResetEvent manualResetEvent1 = new ManualResetEvent(false);
static ManualResetEvent manualResetEvent2 = new ManualResetEvent(false);
static ManualResetEvent manualResetEvent3 = new ManualResetEvent(false);
看WaitAll的測試:
public static void WaitAllTest(){new Thread(new ThreadStart(PrintOne)).Start();new Thread(new ThreadStart(PrintTwo)).Start();new Thread(new ThreadStart(PrintThree)).Start();var isOk = ManualResetEvent.WaitAll(new WaitHandle[] { manualResetEvent1, manualResetEvent2, manualResetEvent3 }, 5000, false);if (isOk){Console.WriteLine("Oh,My God "+isOk);}}輸出結(jié)果:Oh,My God True。
WaitAll是WaitHandle的靜態(tài)方法,它接收WaitHandle數(shù)組,當(dāng)所有的WaitHandle的信號都返回true時,WaitAll才返回True.
而另外一個方法 WaitAny()方法與WaitAll的區(qū)別是,WaitAny當(dāng)數(shù)組中一個WaitHandle獲得信號的時候,就會返回,返回值為數(shù)組中有信號的WaitHandle的索引,當(dāng)全部沒有返回信號時,返回的是System.Threading.WaitHandle.WaitTimeout:
演示代碼如下:
var hasSignalIndex = ManualResetEvent.WaitAny(new WaitHandle[] { manualResetEvent1, manualResetEvent2, manualResetEvent3 }, 2000, false);
if (hasSignalIndex == System.Threading.WaitHandle.WaitTimeout)
{
Console.WriteLine("Oh, fail");
}
else
{
Console.WriteLine("Oh,My God " + hasSignalIndex);
}
?7.2 使用AutoResetEvent同步 --- 與ManualResetEvent 類似,不做演示。
?
?7.3 使用Mutex線程同步
對象:線程
使用方式:一次只有一個線程能夠獲得鎖,只有獲得鎖的線程釋放了鎖其他的線程才能夠獲得鎖。
?
代碼演示:
static Mutex myMutex;public static void MutexThread(){myMutex = new Mutex(true, "myMutex");new Thread(new ThreadStart(PrintNo)).Start();for (int i = 6; i < 10; i++){Console.WriteLine(i);}myMutex.ReleaseMutex();}private static void PrintNo(){myMutex.WaitOne();for (int i = 0; i < 6; i++){Console.WriteLine(i);}}輸出結(jié)果:
6 7 8 9 0 1 2 3 4 5在上面的演示中,主線程先獲得鎖,接著啟動線程,但是線程是沒有鎖的,所以先執(zhí)行主線程的打印循環(huán),當(dāng)主線程釋放鎖的時候,線程才獲得鎖,這時候線程才執(zhí)行。Mutex.WaitOne也有其他的重載方法,可自行探索。
?7.4 使用InterLocked同步Int類型變量:
對象:Int類型變量;
使用方式:InterLocked.Increment(ref a)等方法;
代碼演示:
private static int a = 0;public static void InterLockThread(){for (int i = 0; i < 100; i++){new Thread(new ThreadStart(IncreaseInt)).Start();}for (int i = 0; i < 100; i++){new Thread(new ThreadStart(IncreaseInt)).Start();}}private static void IncreaseInt(){for (int i = 0; i < 1000; i++){//a++;Interlocked.Increment(ref a);}Console.WriteLine(string.Format("Thread:{0} value: {1}", Thread.CurrentThread.GetHashCode(), a));}在上面的 IncreaseInt 方法中有 a++ 和 Interlocked.Increment(ref a)兩種方式,其中 Interlocked.Increment(ref a) 是原子操作。可能有人會想a++也才一句語句,怎么會不是原子操作,但是實際上我們的程序最后都是編譯成了計算機(jī)能夠認(rèn)得的計算機(jī)指令,一句a++是有很多指令組成的。
結(jié)果演示:可以自己試著把a(bǔ)++和?Interlocked.Increment(ref a)切換注釋一下運(yùn)行,結(jié)果很明顯
?
?7.5 使用ThreadStatic屬性為類的靜態(tài)變量創(chuàng)建副本,使得每一個線程的變量獨立
對象:類靜態(tài)變量;
使用方式:把要設(shè)置多個副本的類靜態(tài)變量標(biāo)記屬性【ThreadStatic】
效果:標(biāo)記了ThreadStatic的類靜態(tài)變量各個線程獨立不會因為其他線程堆值的改變而共享。
代碼演示:
[ThreadStatic]static int x;static int y;public static void StaticAttributeThread(){Task.Run(() => { IncreaseInt(); });Task.Run(() => { IncreaseInt(); });}private static void IncreaseInt(){for (int i = 0; i < 5; i++){Console.WriteLine(string.Format("current thread {0},x={1}, y={2}",Thread.CurrentThread.GetHashCode(),x++,y++));}}輸出結(jié)果:
current thread 3,x=0, y=0 current thread 3,x=1, y=2 current thread 3,x=2, y=3 current thread 3,x=3, y=4 current thread 3,x=4, y=5 current thread 4,x=0, y=1 current thread 4,x=1, y=6 current thread 4,x=2, y=7 current thread 4,x=3, y=8 current thread 4,x=4, y=9可以看到,在不同的線程,y值是共享了修改結(jié)果的,而x是沒有的。
?
?
?
?
?
轉(zhuǎn)載于:https://www.cnblogs.com/heisehenbai/p/9960978.html
總結(jié)
以上是生活随笔為你收集整理的C#线程 ---- 线程同步详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaScript DOM介绍
- 下一篇: day21 pickle json sh