并發(fā)編程 - 線程同步(一)
經(jīng)過前面對(duì)線程的嘗試使用,我們對(duì)線程的了解又進(jìn)一步加深了。今天我們繼續(xù)來深入學(xué)習(xí)線程的新知識(shí) —— 線程同步。

01、什么是線程同步
線程同步是指在多線程環(huán)境下,確保多個(gè)線程在同時(shí)使用共享資源時(shí)不會(huì)發(fā)生沖突或數(shù)據(jù)不一致問題的技術(shù),保證線程間的正確協(xié)作。它的目的是使得多個(gè)線程在執(zhí)行過程中能夠按照某種順序、安全地使用共享資源。
02、為何需要線程同步
1、避免競(jìng)爭(zhēng)條件
不知道大家還記得在《并發(fā)編程 - 初識(shí)線程》中出現(xiàn)的關(guān)鍵字volatile和特性ThreadStatic嗎?它們都是為了解決多線程共享資源問題。
在多線程中當(dāng)多個(gè)線程需要同時(shí)使用共享資源時(shí),很容易產(chǎn)生互相競(jìng)爭(zhēng)資源使用權(quán)的情況,這一問題也叫競(jìng)爭(zhēng)條件。此時(shí)就可以通過線程同步技術(shù)實(shí)現(xiàn)多個(gè)線程按順序使用共享資源,從而避免競(jìng)爭(zhēng)條件。
2、保證共享資源安全
我們舉個(gè)簡(jiǎn)單的例子,假如我的銀行賬戶里有1000元,此時(shí)我正在用電子銀行在線上操作準(zhǔn)備向我老婆的賬戶里轉(zhuǎn)賬100元,而恰巧此時(shí)我老婆拿著我的銀行卡準(zhǔn)備取款500。
假如銀行系統(tǒng)還是一個(gè)只有多線程,沒有線程同步功能的老系統(tǒng),在這一前置條件下。假如恰巧我們倆在同一瞬間點(diǎn)了確認(rèn)操作,相信此時(shí)系統(tǒng)會(huì)發(fā)生什么?
有可能會(huì)是系統(tǒng)同時(shí)收到我們倆的請(qǐng)求,此時(shí)我的操作線程A,首先讀取我賬戶余額1000,然后執(zhí)行轉(zhuǎn)賬操作把余額減100得到900,再更新至余額中。而我老婆的操作線程B因?yàn)槭呛臀彝瑫r(shí)的,所以在讀取我賬戶余額的時(shí)候得到的也是1000,而不是900,此時(shí)線程B執(zhí)行取款500操作把余額減500得到500,再更新至余額中。
可以發(fā)現(xiàn)我們倆最后更新余額,無論誰更新成功最后結(jié)果都是不正確的。這個(gè)例子就導(dǎo)致銀行賬戶余額最終不正確,也就是我們說的共享資源不安全。如果使用線程同步,使得線程A、B可以按順序執(zhí)行,無論誰先執(zhí)行最終結(jié)果都會(huì)是正確的。
下面我們?cè)賮斫Y(jié)合代碼舉一個(gè)經(jīng)典問題 —— torn read。
先解釋一下什么叫torn read,可以翻譯成一次讀取被撕成兩半。或者說在機(jī)器級(jí)別上,要分兩個(gè)MOV指令才能讀完。
具體來說就是一個(gè)long類型變量_var,當(dāng)一個(gè)線程把_var賦值為0x0123456789ABCDEF,而此時(shí)另一個(gè)線程來讀取_var,結(jié)果讀取的值是0x0123456700000000或0x0000000089ABCDEF。這同樣是因?yàn)槎嗑€程導(dǎo)致的共享資源不安全問題。
下面看看模擬代碼實(shí)現(xiàn)效果:
public class ThreadSync
{
//共享的int64變量
public static long _var;
public static void Run()
{
//啟動(dòng)寫入線程
var writerThread = new Thread(WriteToSharedValue);
//啟動(dòng)讀取線程
var readerThread = new Thread(ReadFromSharedValue);
//啟動(dòng)線程
writerThread.Start();
readerThread.Start();
//等待線程執(zhí)行完成
writerThread.Join();
readerThread.Join();
}
//寫入線程
static void WriteToSharedValue()
{
//模擬分兩步寫入
long high = 0x01234567;
long low = 0x89ABCDEF;
unsafe
{
//將 _var 分成高低兩部分寫入
//寫高 32 位
_var = high << 32;
// 確保讀取線程能在這里讀取中間值
Thread.Sleep(0);
//寫低 32 位
_var |= low;
}
Console.WriteLine($"寫: 寫入值 0x{_var:X16}");
}
//讀取線程
static void ReadFromSharedValue()
{
// 讀取共享變量的值
Console.WriteLine($"讀: 讀取值 0x{_var:X16}");
}
}
我們看下執(zhí)行效果:

當(dāng)然上面的例子并不是每次都會(huì)出現(xiàn)的,可能需要多運(yùn)行幾次,另外關(guān)于寫入線程為什么不是直接賦值而是把值拆成高低位分兩次寫入?
這是因?yàn)槲业碾娔X是64位系統(tǒng),在大多數(shù)現(xiàn)代的 x64 系統(tǒng)架構(gòu)(例如 Intel 和 AMD 處理器)上,64 位的原子性操作通常是被保證的。即使對(duì)于像 long(64 位)這種數(shù)據(jù)類型,處理器通常會(huì)在硬件層面確保它的讀寫操作是原子性的,因此,不太容易發(fā)生撕裂的讀(torn read)。
所以這里的代碼把一次賦值行為認(rèn)為拆解成兩步,同時(shí)Thread.Sleep(0)也為了讓當(dāng)前線程主動(dòng)讓出 CPU 時(shí)間片,使讀線程有機(jī)會(huì)讀取,使其更貼近在x32環(huán)境下運(yùn)行的情況。如果有條件可以用直接賦值再x32環(huán)境下看看效果。
03、如何實(shí)現(xiàn)線程同步
1、避免資源共享
當(dāng)然嚴(yán)格意義上說可能這一條不算是線程同步,只能說解決了多線程碰到的問題,達(dá)到線程同步的效果。
如果沒有共享資源,那么自然就無須進(jìn)行線程同步。大多數(shù)時(shí)候可以通過重新設(shè)計(jì)程序來除移共享狀態(tài),從而去掉復(fù)雜的同步構(gòu)造。盡可能避免在多個(gè)線程間使用單一對(duì)象。
除了通過重新設(shè)計(jì)來移除共享狀態(tài),還可以通過語言特性設(shè)計(jì)使其達(dá)到無共享狀態(tài)。比如值類型在傳遞過程中總是被復(fù)制,每個(gè)線程都會(huì)有自己的數(shù)據(jù)副本,比如看下面這個(gè)方法:
public static int Max(int val1, int val2)
{
return val1 > val2 ? val1 : val2;
}
即使這個(gè)方法沒有使用任何線程同步方法,這個(gè)方法也是線程安全的。因?yàn)橹殿愋吞匦栽颍詡鹘oMax的兩個(gè)int值會(huì)復(fù)制到方法內(nèi)部,形成自己的數(shù)據(jù)副本。此時(shí)無論有多少個(gè)線程調(diào)用Max方法,每個(gè)線程處理的都是它自己的數(shù)據(jù),線程之間并不會(huì)互相干擾。
2、用戶模式同步機(jī)制
用戶模式同步機(jī)制指在用戶空間內(nèi)完成線程的阻塞和喚醒操作,由程序自己管理同步對(duì)象的一種同步方式,因?yàn)椴簧婕芭c操作系統(tǒng)內(nèi)核交換,因此開銷較低,更輕量級(jí)。
實(shí)現(xiàn)方式有SpinLock、SpinWait、Monitor(lock)等。

3、內(nèi)核模式同步機(jī)制
內(nèi)核模式同步機(jī)制是指在操作系統(tǒng)內(nèi)核空間就完成線程的掛起與恢復(fù),由操作系統(tǒng)管理同步對(duì)象的一種同步方式,因?yàn)槊看尉€程同步操作都需要操作系統(tǒng)參與,因此必然回涉及內(nèi)核態(tài)的上下文切換,同時(shí)還是涉及到操作系統(tǒng)內(nèi)部的數(shù)據(jù)結(jié)構(gòu)和資源管理,因此內(nèi)核模式同步機(jī)制往往會(huì)導(dǎo)致較高的開銷。
實(shí)現(xiàn)方式有Semaphore、Mutex、AutoResetEvent等。
4、混合模式同步機(jī)制
混合模式同步機(jī)制在某些情況下會(huì)根據(jù)線程競(jìng)爭(zhēng)的情況在用戶模式和內(nèi)核模式之間切換。通常,當(dāng)資源訪問沖突較小或線程阻塞較少時(shí),采用用戶模式同步;當(dāng)資源爭(zhēng)用較多或有較大的線程等待時(shí),自動(dòng)切換到內(nèi)核模式同步。
實(shí)現(xiàn)方式有SemaphoreSlim、ManualResetEventSlim、CountDownEvent、Barrier、ReaderWriterLockSlim等。
注:測(cè)試方法代碼以及示例源碼都已經(jīng)上傳至代碼庫(kù),有興趣的可以看看。https://gitee.com/hugogoos/Planner

浙公網(wǎng)安備 33010602011771號(hào)