線程同步概念想要解決的問題?
應用程序里面多個線程的存在引發(fā)了多個執(zhí)行線程安全訪問資源的潛在問題芝硬。兩個線程同時修改同一資源有可能以意想不到的方式互相干擾蚜点。這個現(xiàn)象被稱為"數(shù)據(jù)競爭"。
線程之間不能且不應該是完全獨立的拌阴,互不干擾的(要不設計來干嘛绍绘?)。所以在線程必須交互的情況下,你需要使用同步工具陪拘,來確保當它們交互的時候是安全的厂镇。
同步工具
為了防止不同線程意外修改數(shù)據(jù),你可以設計你的程序沒有同步問題左刽,或你也可以使用同步工具捺信。盡管完全避免出現(xiàn)同步問題相對更好一點,但是幾乎總是無法實現(xiàn)悠反。
1.原子操作
原子操作是同步的一個簡單的形式残黑,它處理簡單的數(shù)據(jù)類型。原子操作的優(yōu)勢是它們不妨礙競爭的線程斋否。對于簡單的操作梨水,比如遞增一個計數(shù)器,原子操作比使用鎖具有更高的性能優(yōu)勢茵臭。
2.內(nèi)存屏障和Volatile變量
內(nèi)存屏障(memory barrier)是一個使用來確保內(nèi)存操作按照正確的順序工作的非阻塞的同步工具疫诽。內(nèi)存屏障的作用就像一個柵欄,迫使處理器來完成位于障礙前面的任何加載和存儲操作旦委,才允許它執(zhí)行位于屏障之后的加載和存儲操作奇徒。內(nèi)存屏障同樣使用來確保一個線程(但對另外一個線程可見)的內(nèi)存操作總是按照預定的順序完成。如果在這些地方缺少內(nèi)存屏障有可能讓其他線程看到看似不可能的結果(比如缨硝,內(nèi)存屏障的維基百科條目)摩钙。為了使用一個內(nèi)存屏障,你只要在你代碼里面需要的地方簡單的調(diào)用OSMemoryBarrier函數(shù)查辩。
Volatile 變量適用于獨立變量的另一個內(nèi)存限制類型胖笛。編譯器優(yōu)化代碼通過加載這些變量的值進入寄存器。對于本地變量宜岛,這通常不會有什么問題长踊。但是如果一個變量對另外一個線程可見,那么這種優(yōu)化可能會阻止其他線程發(fā)現(xiàn)變量的任何變化萍倡。在變量之前加上關鍵字volatile可以強制編譯器每次使用變量的時候都從內(nèi)存里面加載身弊。如果一個變量的值隨時可能給編譯器無法檢測的外部源更改,那么你可以把該變量聲明為volatile變量列敲。
因為內(nèi)存屏障和volatile變量降低了編譯器可執(zhí)行的優(yōu)化阱佛,因此你應該謹慎使用它們,只在有需要的地方時候戴而,以確保正確性瘫絮。
3.鎖
- Mutex(互斥鎖)
一個互斥鎖對于資源來說很像是一個保護性的barrier。它是一種信號量機制填硕,在同一時間只允許一個進程來訪問資源。如果一個互斥鎖正在被一個線程獲取使用,另外一個線程如果想要使用的話就必須等待holder線程釋放這個鎖扁眯。多個一期請求的時候也只會有一個被賦予權限壮莹。
- Recursive lock(遞歸鎖)
遞歸鎖其實是互斥鎖的一個變種。就是一個線程可以多次持有一個鎖姻檀,其它線程像訪問的時候命满,這個線程必須把這個多次持有都釋放了。應用場景是多個方法绣版,每個都要單獨獲取鎖胶台。
- Read-write lock(讀寫鎖)
共享互斥鎖。這個在客戶端和服務端有不同杂抽,對服務端而言诈唬,一個連接或者請求就是一個并發(fā)操作,而客戶端缩麸,因為只有一個人铸磅,并發(fā)操作多指一個程序進程內(nèi)的多個場景。就是多個讀操作可以一起整杭朱,而寫操作就必須等所有讀操作都完了阅仔。共享,并互斥弧械。只有POSIX threads才支持八酒。
- Distributed lock(分布鎖)
提供了process級別的互斥access。它不會真的把process鎖住刃唐。只是提個建議羞迷,告訴process自己最近很忙,要不要停一停唁桩。
- Spin lock(自旋鎖)
它會反復poll自己的lock conditon闭树,直到lock conditon 變成 true。多是設計多核心應用切換時候用的荒澡。
- Double-checked lock(雙重檢查鎖)
來回監(jiān)測报辱,不安全,系統(tǒng)不提供
4.條件
條件是信號量的另外一個形式单山,它允許在條件為真的時候線程間互相發(fā)送信號。條件通常被使用來說明資源可用性米奸,或用來確保任務以特定的順序執(zhí)行昼接。當一個線程測試一個條件時悴晰,它會被阻塞直到條件為真。它會一直阻塞直到其他線程顯式的修改信號量的狀態(tài)泪喊。條件和互斥鎖(mutex lock)的區(qū)別在于多個線程被允許同時訪問一個條件袒啼。條件更多是允許不同線程根據(jù)一些指定的標準通過的守門人蚓再。
一個方式是你使用條件來管理掛起事件的池包各。事件隊列可能使用條件變量來給等待線程發(fā)送信號摘仅,此時它們在事件隊列中的時候。如果一個事件到達時髓棋,隊列將給條件發(fā)送合適信號实檀。如果一個線程已經(jīng)處于等待,它會被喚醒按声,屆時它將會取出事件并處理它膳犹。如果兩個事件到達隊列的時間大致相同,隊列將會發(fā)送兩次信號喚醒兩個線程签则。
系統(tǒng)通過幾個不同的技術來支持條件须床。
5.執(zhí)行slector
Cocoa程序包含了一個在一個線程以同步的方式傳遞消息的方便方法。NSObject類聲明方法來在應用的一個活動線程上面執(zhí)行selector的方法渐裂。這些方法允許你的線程以異步的方式來傳遞消息豺旬,以確保它們在同一個線程上面執(zhí)行是同步的。比如柒凉,你可以通過執(zhí)行selector消息來把一個從你分布計算的結果傳遞給你的應用的主線程或其他目標線程族阅。每個執(zhí)行selector的請求都會被放入一個目標線程的run loop的隊列里面,然后請求會按照它們到達的順序被目標線程有序的處理膝捞。
同步的成本
同步幫助確保你代碼的正確性坦刀,但同時將會犧牲部分性能。甚至在無競態(tài)的情況下蔬咬,同步工具的使用將在后面介紹鲤遥。鎖和原子操作通常包含了內(nèi)存屏障和內(nèi)核級別同步的使用來確保代碼正確被保護。如果林艘,發(fā)生鎖的爭奪盖奈,你的線程有可能進入阻塞,在體驗上會產(chǎn)生更大的遲延狐援。
- mutex獲得時間(0.2ms)
這個時間是沒有競爭條件下的钢坦。如果lock被另一個線程持有究孕,時間就更長了。
- Atomic compare-and-swap(0.05)
如何進行同步(注意事項)
- 當心死鎖和活鎖
死鎖:當兩個不同的線程分別保持一個鎖(而該鎖是另外一個線程需要的)又試圖獲得另外線程保持的鎖時就會發(fā)生死鎖场钉。結果是每個線程都會進入持久性阻塞狀態(tài)蚊俺,因為它永遠不可能獲得另外那個鎖。
活鎖:當兩個線程競爭同一個資源的時候就可能發(fā)生活鎖逛万。在發(fā)生活鎖的情況里,一個線程放棄它的第一個鎖并試圖獲得第二個鎖批钠。一旦它獲得第二個鎖宇植,它返回并試圖再次獲得一個鎖。線程就會被鎖起來埋心,因為它花費所有的時間來釋放一個鎖指郁,并試圖獲取其他鎖,而不做實際的工作拷呆。
- 正確使用Volatile變量
如果你已經(jīng)使用了一個互斥鎖來保護一個代碼段闲坎,不要自動假設你需要使用關鍵詞volatile來保護該代碼段的重要的變量。一個互斥鎖包含了內(nèi)存屏障來確保加載和存儲操作是按照正確順序的茬斧。在一個臨界區(qū)添加關鍵字volatile到變量上面會強制每次訪問該變量的時候都要從內(nèi)存里面從加載腰懂。這兩種同步技巧的組合使用在一些特定區(qū)域是必須的,但是同樣會導致顯著的性能損失项秉。如果單獨使用互斥鎖已經(jīng)可以保護變量绣溜,那么忽略關鍵字volatile。
為了避免使用互斥鎖而不使用volatile變量同樣很重要娄蔼。通常情況下怖喻,互斥鎖和其他同步機制是比volatile變量更好的方式來保護數(shù)據(jù)結構的完整性。關鍵字volatile只是確保從內(nèi)存加載變量而不是使用寄存器里面的變量岁诉。它不保證你代碼訪問變量是正確的锚沸。
- 使用NSLock或相關協(xié)議類
互斥鎖要注意的是同一個鎖對象,被多個線程獲取的時候涕癣,多個線程都需要等待釋放哗蜈,才能分配執(zhí)行。@synchorize其實是和NSLock一樣的東西属划,獲取了一個對象相關連的鎖恬叹, 但是編譯階段優(yōu)化回去還是個mutex。當一個線程想要多次獲得一個鎖對象的時候要用遞歸鎖同眯,要不就deadlock绽昼,大家可以挑一個。
- @synchorize
功能上像個語法糖须蜗。使用起來很簡單硅确。但是有這么個過程目溉,就是作為一種預防措施,@synchronized塊隱式的添加一個異常處理例程來保護代碼菱农。該處理例程會在異常拋出的時候自動的釋放互斥鎖缭付。這意味著為了使用@synchronized指令,你必須在你的代碼中啟用異常處理循未。了如果你不想讓隱式的異常處理例程帶來額外的開銷陷猫,你應該考慮使用鎖的類。
- 使用條件
下篇有個簡單的例子
- 信號量機制
邏輯上是個PV操作可控制的值的妖,具體使用就很多了绣檬。希望有機會補上。