線程同步概念想要解決的問題?
應(yīng)用程序里面多個線程的存在引發(fā)了多個執(zhí)行線程安全訪問資源的潛在問題。兩個線程同時(shí)修改同一資源有可能以意想不到的方式互相干擾。這個現(xiàn)象被稱為"數(shù)據(jù)競爭"。
線程之間不能且不應(yīng)該是完全獨(dú)立的,互不干擾的(要不設(shè)計(jì)來干嘛?)。所以在線程必須交互的情況下,你需要使用同步工具,來確保當(dāng)它們交互的時(shí)候是安全的。
同步工具
為了防止不同線程意外修改數(shù)據(jù),你可以設(shè)計(jì)你的程序沒有同步問題,或你也可以使用同步工具。盡管完全避免出現(xiàn)同步問題相對更好一點(diǎn),但是幾乎總是無法實(shí)現(xiàn)。
1.原子操作
原子操作是同步的一個簡單的形式,它處理簡單的數(shù)據(jù)類型。原子操作的優(yōu)勢是它們不妨礙競爭的線程。對于簡單的操作,比如遞增一個計(jì)數(shù)器,原子操作比使用鎖具有更高的性能優(yōu)勢。
2.內(nèi)存屏障和Volatile變量
內(nèi)存屏障(memory barrier)是一個使用來確保內(nèi)存操作按照正確的順序工作的非阻塞的同步工具。內(nèi)存屏障的作用就像一個柵欄,迫使處理器來完成位于障礙前面的任何加載和存儲操作,才允許它執(zhí)行位于屏障之后的加載和存儲操作。內(nèi)存屏障同樣使用來確保一個線程(但對另外一個線程可見)的內(nèi)存操作總是按照預(yù)定的順序完成。如果在這些地方缺少內(nèi)存屏障有可能讓其他線程看到看似不可能的結(jié)果(比如,內(nèi)存屏障的維基百科條目)。為了使用一個內(nèi)存屏障,你只要在你代碼里面需要的地方簡單的調(diào)用OSMemoryBarrier函數(shù)。
Volatile 變量適用于獨(dú)立變量的另一個內(nèi)存限制類型。編譯器優(yōu)化代碼通過加載這些變量的值進(jìn)入寄存器。對于本地變量,這通常不會有什么問題。但是如果一個變量對另外一個線程可見,那么這種優(yōu)化可能會阻止其他線程發(fā)現(xiàn)變量的任何變化。在變量之前加上關(guān)鍵字volatile可以強(qiáng)制編譯器每次使用變量的時(shí)候都從內(nèi)存里面加載。如果一個變量的值隨時(shí)可能給編譯器無法檢測的外部源更改,那么你可以把該變量聲明為volatile變量。
因?yàn)閮?nèi)存屏障和volatile變量降低了編譯器可執(zhí)行的優(yōu)化,因此你應(yīng)該謹(jǐn)慎使用它們,只在有需要的地方時(shí)候,以確保正確性。
3.鎖
- Mutex(互斥鎖)
一個互斥鎖對于資源來說很像是一個保護(hù)性的barrier。它是一種信號量機(jī)制,在同一時(shí)間只允許一個進(jìn)程來訪問資源。如果一個互斥鎖正在被一個線程獲取使用,另外一個線程如果想要使用的話就必須等待holder線程釋放這個鎖。多個一期請求的時(shí)候也只會有一個被賦予權(quán)限。
- Recursive lock(遞歸鎖)
遞歸鎖其實(shí)是互斥鎖的一個變種。就是一個線程可以多次持有一個鎖,其它線程像訪問的時(shí)候,這個線程必須把這個多次持有都釋放了。應(yīng)用場景是多個方法,每個都要單獨(dú)獲取鎖。
- Read-write lock(讀寫鎖)
共享互斥鎖。這個在客戶端和服務(wù)端有不同,對服務(wù)端而言,一個連接或者請求就是一個并發(fā)操作,而客戶端,因?yàn)橹挥幸粋€人,并發(fā)操作多指一個程序進(jìn)程內(nèi)的多個場景。就是多個讀操作可以一起整,而寫操作就必須等所有讀操作都完了。共享,并互斥。只有POSIX threads才支持。
- Distributed lock(分布鎖)
提供了process級別的互斥access。它不會真的把process鎖住。只是提個建議,告訴process自己最近很忙,要不要停一停。
- Spin lock(自旋鎖)
它會反復(fù)poll自己的lock conditon,直到lock conditon 變成 true。多是設(shè)計(jì)多核心應(yīng)用切換時(shí)候用的。
- Double-checked lock(雙重檢查鎖)
來回監(jiān)測,不安全,系統(tǒng)不提供
4.條件
條件是信號量的另外一個形式,它允許在條件為真的時(shí)候線程間互相發(fā)送信號。條件通常被使用來說明資源可用性,或用來確保任務(wù)以特定的順序執(zhí)行。當(dāng)一個線程測試一個條件時(shí),它會被阻塞直到條件為真。它會一直阻塞直到其他線程顯式的修改信號量的狀態(tài)。條件和互斥鎖(mutex lock)的區(qū)別在于多個線程被允許同時(shí)訪問一個條件。條件更多是允許不同線程根據(jù)一些指定的標(biāo)準(zhǔn)通過的守門人。
一個方式是你使用條件來管理掛起事件的池。事件隊(duì)列可能使用條件變量來給等待線程發(fā)送信號,此時(shí)它們在事件隊(duì)列中的時(shí)候。如果一個事件到達(dá)時(shí),隊(duì)列將給條件發(fā)送合適信號。如果一個線程已經(jīng)處于等待,它會被喚醒,屆時(shí)它將會取出事件并處理它。如果兩個事件到達(dá)隊(duì)列的時(shí)間大致相同,隊(duì)列將會發(fā)送兩次信號喚醒兩個線程。
系統(tǒng)通過幾個不同的技術(shù)來支持條件。
5.執(zhí)行slector
Cocoa程序包含了一個在一個線程以同步的方式傳遞消息的方便方法。NSObject類聲明方法來在應(yīng)用的一個活動線程上面執(zhí)行selector的方法。這些方法允許你的線程以異步的方式來傳遞消息,以確保它們在同一個線程上面執(zhí)行是同步的。比如,你可以通過執(zhí)行selector消息來把一個從你分布計(jì)算的結(jié)果傳遞給你的應(yīng)用的主線程或其他目標(biāo)線程。每個執(zhí)行selector的請求都會被放入一個目標(biāo)線程的run loop的隊(duì)列里面,然后請求會按照它們到達(dá)的順序被目標(biāo)線程有序的處理。
同步的成本
同步幫助確保你代碼的正確性,但同時(shí)將會犧牲部分性能。甚至在無競態(tài)的情況下,同步工具的使用將在后面介紹。鎖和原子操作通常包含了內(nèi)存屏障和內(nèi)核級別同步的使用來確保代碼正確被保護(hù)。如果,發(fā)生鎖的爭奪,你的線程有可能進(jìn)入阻塞,在體驗(yàn)上會產(chǎn)生更大的遲延。
- mutex獲得時(shí)間(0.2ms)
這個時(shí)間是沒有競爭條件下的。如果lock被另一個線程持有,時(shí)間就更長了。
- Atomic compare-and-swap(0.05)
如何進(jìn)行同步(注意事項(xiàng))
- 當(dāng)心死鎖和活鎖
死鎖:當(dāng)兩個不同的線程分別保持一個鎖(而該鎖是另外一個線程需要的)又試圖獲得另外線程保持的鎖時(shí)就會發(fā)生死鎖。結(jié)果是每個線程都會進(jìn)入持久性阻塞狀態(tài),因?yàn)樗肋h(yuǎn)不可能獲得另外那個鎖。
活鎖:當(dāng)兩個線程競爭同一個資源的時(shí)候就可能發(fā)生活鎖。在發(fā)生活鎖的情況里,一個線程放棄它的第一個鎖并試圖獲得第二個鎖。一旦它獲得第二個鎖,它返回并試圖再次獲得一個鎖。線程就會被鎖起來,因?yàn)樗ㄙM(fèi)所有的時(shí)間來釋放一個鎖,并試圖獲取其他鎖,而不做實(shí)際的工作。
- 正確使用Volatile變量
如果你已經(jīng)使用了一個互斥鎖來保護(hù)一個代碼段,不要自動假設(shè)你需要使用關(guān)鍵詞volatile來保護(hù)該代碼段的重要的變量。一個互斥鎖包含了內(nèi)存屏障來確保加載和存儲操作是按照正確順序的。在一個臨界區(qū)添加關(guān)鍵字volatile到變量上面會強(qiáng)制每次訪問該變量的時(shí)候都要從內(nèi)存里面從加載。這兩種同步技巧的組合使用在一些特定區(qū)域是必須的,但是同樣會導(dǎo)致顯著的性能損失。如果單獨(dú)使用互斥鎖已經(jīng)可以保護(hù)變量,那么忽略關(guān)鍵字volatile。
為了避免使用互斥鎖而不使用volatile變量同樣很重要。通常情況下,互斥鎖和其他同步機(jī)制是比volatile變量更好的方式來保護(hù)數(shù)據(jù)結(jié)構(gòu)的完整性。關(guān)鍵字volatile只是確保從內(nèi)存加載變量而不是使用寄存器里面的變量。它不保證你代碼訪問變量是正確的。
- 使用NSLock或相關(guān)協(xié)議類
互斥鎖要注意的是同一個鎖對象,被多個線程獲取的時(shí)候,多個線程都需要等待釋放,才能分配執(zhí)行。@synchorize其實(shí)是和NSLock一樣的東西,獲取了一個對象相關(guān)連的鎖, 但是編譯階段優(yōu)化回去還是個mutex。當(dāng)一個線程想要多次獲得一個鎖對象的時(shí)候要用遞歸鎖,要不就deadlock,大家可以挑一個。
- @synchorize
功能上像個語法糖。使用起來很簡單。但是有這么個過程,就是作為一種預(yù)防措施,@synchronized塊隱式的添加一個異常處理例程來保護(hù)代碼。該處理例程會在異常拋出的時(shí)候自動的釋放互斥鎖。這意味著為了使用@synchronized指令,你必須在你的代碼中啟用異常處理。了如果你不想讓隱式的異常處理例程帶來額外的開銷,你應(yīng)該考慮使用鎖的類。
- 使用條件
下篇有個簡單的例子
- 信號量機(jī)制
邏輯上是個PV操作可控制的值,具體使用就很多了。希望有機(jī)會補(bǔ)上。