本文選譯自《Threading Programming Guide》垫释。
導(dǎo)語(yǔ)
線程技術(shù)作為在單個(gè)應(yīng)用程序中并發(fā)執(zhí)行多個(gè)代碼路徑的技術(shù)之一显蝌。盡管新的技術(shù)骆撇,諸如操作對(duì)象(Operation objects)和大中樞調(diào)度(GCD)提供了一個(gè)更加現(xiàn)代和高效的并發(fā)實(shí)現(xiàn)方式,但OS X和iOS也提供了用于創(chuàng)建和管理線程的接口。
注意:如果你正在開(kāi)發(fā)一個(gè)新的應(yīng)用,你可以選擇使用該技術(shù)作為并發(fā)操作的實(shí)現(xiàn)之一运授。假使你并未真正理解該技術(shù)對(duì)于實(shí)現(xiàn)多線程應(yīng)用的技術(shù)細(xì)節(jié)渡贾,這里有一些簡(jiǎn)化并發(fā)操作實(shí)現(xiàn)難度并提供性能更加優(yōu)越的技術(shù)方案可供選擇纺讲。獲取更多信息熬甚,請(qǐng)參見(jiàn)《Concurrency Programming Guide》。
本文結(jié)構(gòu)
- 關(guān)于線程編程
- 線程管理
- Run Loops
- 線程同步
- 線程安全總結(jié)
關(guān)于線程編程
多年以來(lái)诲泌,計(jì)算機(jī)性能峰值在很大程度上受制于單個(gè)微處理器的計(jì)算機(jī)核心。當(dāng)單個(gè)處理器的速度開(kāi)始達(dá)到它們的實(shí)際限制時(shí),芯片制造商切換到多核設(shè)計(jì)羹幸,以達(dá)到讓計(jì)算機(jī)同時(shí)執(zhí)行多個(gè)任務(wù)的目的。雖然操作系統(tǒng)可以利用多核技術(shù)來(lái)執(zhí)行系統(tǒng)相關(guān)的任務(wù),然而你自己的應(yīng)用也可以通過(guò)線程技術(shù)來(lái)利用該技術(shù)而芥。
何為線程误辑?
線程是程序中一個(gè)更加輕量級(jí)的多路徑執(zhí)行實(shí)現(xiàn)方式。在系統(tǒng)級(jí)別砰苍,程序會(huì)根據(jù)系統(tǒng)為其提供的執(zhí)行時(shí)間以及其他程序需要的執(zhí)行時(shí)間統(tǒng)一調(diào)度執(zhí)行。在程序內(nèi)部,存在承載著不同任務(wù)的一個(gè)或多個(gè)同時(shí)或近乎同時(shí)執(zhí)行的線程黍少。實(shí)際上系統(tǒng)本身會(huì)管理線程的執(zhí)行,調(diào)度其運(yùn)行在某個(gè)核心上或在其他線程需要執(zhí)行的時(shí)候強(qiáng)制中段該線程的執(zhí)行。
從技術(shù)層面看访忿,線程是一個(gè)內(nèi)核級(jí)和應(yīng)用級(jí)數(shù)據(jù)結(jié)構(gòu)組合,用于管理代碼的執(zhí)行卧斟。內(nèi)核級(jí)結(jié)構(gòu)協(xié)調(diào)事件的調(diào)度和可用的核心搶占式調(diào)度。應(yīng)用級(jí)結(jié)構(gòu)包括用于存儲(chǔ)函數(shù)調(diào)用的調(diào)用堆棧以及需要管理和操作線程的屬性與狀態(tài)的程序結(jié)構(gòu)。
在非并發(fā)程序中蛋铆,只有一個(gè)線程的執(zhí)行。這個(gè)線程開(kāi)始和結(jié)束于程序的main函數(shù),并由一個(gè)接一個(gè)的不同方法或函數(shù)來(lái)實(shí)現(xiàn)程序的全部行為乳乌。相比之下,支持并發(fā)的程序可以啟動(dòng)一個(gè)線程,并按照需求增加線程以創(chuàng)建額外的執(zhí)行路徑。每一條新路徑都有獨(dú)立的自定義啟動(dòng)入口扳抽,在程序的main函數(shù)中獨(dú)立運(yùn)行代碼。多線程的程序具有兩個(gè)非常重要的潛在優(yōu)勢(shì):
- 多線程可以提高應(yīng)用程序的響應(yīng)能力楞陷。
- 多線程可以提高應(yīng)用程序在多核系統(tǒng)上的實(shí)時(shí)性能。
如果你的應(yīng)用只有一個(gè)線程茉唉,那么單個(gè)線程就必須完成所有的操作固蛾。它必須對(duì)事件作出響應(yīng),更新應(yīng)用窗口度陆,以及完成應(yīng)用行為的全部計(jì)算。單個(gè)線程面臨著同一時(shí)刻只能執(zhí)行一個(gè)任務(wù)的問(wèn)題坚芜,所以當(dāng)其中的一個(gè)計(jì)算任務(wù)需要花費(fèi)很長(zhǎng)時(shí)間來(lái)完成時(shí)怎么辦览芳?當(dāng)你的應(yīng)用忙于計(jì)算,卻停止響應(yīng)用戶事件和更新窗口時(shí)鸿竖。如果這樣的情況長(zhǎng)期持續(xù)下去铸敏,用戶也許會(huì)任務(wù)應(yīng)用被掛起并且會(huì)嘗試強(qiáng)制退出。如果將自定義的計(jì)算操作移至單獨(dú)的線程中去,應(yīng)用程序的主線程才會(huì)在合適的時(shí)機(jī)有機(jī)會(huì)去和用戶做交互。
隨著多核計(jì)算機(jī)的日益普及,線程技術(shù)成為了某些應(yīng)用提供了提升性能的一種方式。線程可以在多核設(shè)備上同時(shí)執(zhí)行不同任務(wù),這使得應(yīng)用程序在同一時(shí)間大大地提高了工作效率房待。
當(dāng)然,線程技術(shù)并不是解決應(yīng)用性能問(wèn)題的萬(wàn)能藥流椒。線程技術(shù)為我們開(kāi)發(fā)帶來(lái)好處的同時(shí)也伴隨著隱患。程序中的多個(gè)執(zhí)行路徑會(huì)給代碼添加相當(dāng)數(shù)量的復(fù)雜度绣硝。每個(gè)線程與其他線程必須協(xié)調(diào)行動(dòng)以防止程序的狀態(tài)信息被破壞。因?yàn)樵趩蝹€(gè)應(yīng)用程序中各線程共享相同的內(nèi)存空間,它們可以訪問(wèn)所有共享的數(shù)據(jù)。如果兩個(gè)線程試圖同時(shí)操縱共享數(shù)據(jù),一個(gè)線程可能會(huì)覆蓋其他線程的修改脯宿,導(dǎo)致數(shù)據(jù)破壞舍肠。即使有適當(dāng)?shù)谋Wo(hù)措施,你仍需要注意編譯器優(yōu)化為代碼引入微妙的(和不那么微妙的)錯(cuò)誤点骑。
相關(guān)術(shù)語(yǔ)
在深入討論線程及其支持技術(shù)之前谍夭,必須定義一些基本術(shù)語(yǔ)。
如果你熟悉UNIX系統(tǒng)紧索,你會(huì)發(fā)現(xiàn)task是由本文檔中使用不同的。在UNIX系統(tǒng)中珠漂,術(shù)語(yǔ)task的使用有時(shí)指一個(gè)正在運(yùn)行的進(jìn)程。
本文采用以下術(shù)語(yǔ):
- 線程(thread)用來(lái)指代碼的一個(gè)單獨(dú)執(zhí)行路徑。
- 進(jìn)程(process)用來(lái)指一個(gè)正在運(yùn)行的可執(zhí)行文件,可包含多個(gè)線程签夭。
- 任務(wù)(task)用來(lái)指需要進(jìn)行工作的抽象概念措拇。
線程替代技術(shù)
自己創(chuàng)建線程的一個(gè)問(wèn)題是它可能會(huì)給你的代碼帶來(lái)不確定性。因?yàn)榫€程是一個(gè)比較低級(jí)別且復(fù)雜的方式來(lái)支持應(yīng)用程序的并發(fā)性慎宾。如果你不完全理解選擇該方式的含義丐吓,你很容易地遭遇線程同步或開(kāi)發(fā)時(shí)間問(wèn)題,其嚴(yán)重程度可以從細(xì)微的行為變化到應(yīng)用的崩潰以及用戶數(shù)據(jù)的破壞趟据。
另一個(gè)需要考慮的因素是應(yīng)用到底是否需要線程或并發(fā)券犁。線程解決了在同一進(jìn)程中同時(shí)執(zhí)行多個(gè)代碼路徑的具體問(wèn)題⌒诩睿可能有先例粘衬,但你所做的工作不需要并發(fā)。線程在你的進(jìn)程中引入巨大的開(kāi)銷咳促,無(wú)論是在內(nèi)存消耗和處理器時(shí)間方面稚新。你可能會(huì)發(fā)現(xiàn),對(duì)于預(yù)定的任務(wù)這個(gè)開(kāi)銷太大跪腹,或者說(shuō)其他選擇更容易實(shí)現(xiàn)褂删。
表1-1 線程替代技術(shù)
名稱 | 描述 |
---|---|
操作對(duì)象(Operation objects) | OS X 10.5引入,操作對(duì)象是對(duì)運(yùn)行在輔助線程上執(zhí)行任務(wù)的封裝冲茸。這層封裝隱藏了執(zhí)行任務(wù)對(duì)于線程的管理屯阀,讓你可以自由地專注于任務(wù)本身。通常轴术,你需要將操作對(duì)象與一個(gè)操作隊(duì)列對(duì)象協(xié)同使用难衰,該操作隊(duì)列對(duì)象實(shí)際上管理一個(gè)或多個(gè)線程中的操作對(duì)象的執(zhí)行。 |
大中樞調(diào)度(GCD) | OS X 10.6引入逗栽,大中樞調(diào)度是另外一種線程技術(shù)盖袭,它讓你專注于你需要執(zhí)行的任務(wù)而不是線程管理。通過(guò)GCD祭陷,你定義需要執(zhí)行的任務(wù)并將其添加到一個(gè)工作隊(duì)列中苍凛,它會(huì)調(diào)度你的任務(wù)在一個(gè)適當(dāng)?shù)木€程上執(zhí)行。工作隊(duì)列會(huì)根據(jù)可用的處理器核心數(shù)量和當(dāng)前的任務(wù)負(fù)載來(lái)執(zhí)行你的任務(wù)兵志,這比你自己使用線程更加高效醇蝴。 |
空閑時(shí)間通知(Idle-time notifications) | 對(duì)于相對(duì)較短且優(yōu)先級(jí)很低的任務(wù),當(dāng)你的應(yīng)用程序不忙的時(shí)候空閑時(shí)間通知會(huì)執(zhí)行你的任務(wù)想罕。Cocoa使用NSNotificationQueue對(duì)象為空閑時(shí)間通知提供支持悠栓。向NSNotificationQueue對(duì)象發(fā)送一個(gè)默認(rèn)選項(xiàng)為NSPostWhenIdle的通知霉涨,即可完成空閑時(shí)間通知的請(qǐng)求。直到run loop處于空閑狀態(tài)時(shí)該隊(duì)列才會(huì)完成對(duì)通知對(duì)象的消息傳遞惭适。 |
異步函數(shù)(Asynchronous functions) | 系統(tǒng)接口包括許多為你提供自動(dòng)并發(fā)的異步函數(shù)笙瑟。這些API會(huì)使用系統(tǒng)守護(hù)進(jìn)程和進(jìn)程或創(chuàng)建自定義線程來(lái)執(zhí)行任務(wù)并返回結(jié)果給你。(實(shí)際的實(shí)現(xiàn)是不相關(guān)的癞志,因?yàn)樗菑哪愕拇a中分離出來(lái)的)往枷,當(dāng)設(shè)計(jì)你的應(yīng)用時(shí),尋找提供異步行為的函數(shù)凄杯,并考慮使用它們错洁,而不是使用自定義線程上的等效同步函數(shù)。 |
定時(shí)器(Timers) | 你可以用定時(shí)器在應(yīng)用程序的主線程上周期性地執(zhí)行一些太過(guò)瑣碎而不需要?jiǎng)?chuàng)建線程戒突,但仍需要定期進(jìn)行的任務(wù)屯碴。 |
獨(dú)立進(jìn)程(Separate processes) | 雖然比線程更重量級(jí),創(chuàng)建一個(gè)單獨(dú)的進(jìn)程在可能的情況下是為了完成與應(yīng)用相切的任務(wù)膊存。如果任務(wù)需要大量的內(nèi)存或者必須以root權(quán)限執(zhí)行导而,你需要使用一個(gè)進(jìn)程。例如隔崎,你可以使用64位服務(wù)器進(jìn)程來(lái)計(jì)算一個(gè)大數(shù)據(jù)集今艺,而讓32位應(yīng)用為用戶顯示結(jié)果。 |
線程支持
如果你的代碼中已經(jīng)使用了線程仍稀,OS X和iOS提供了多個(gè)為應(yīng)用創(chuàng)建線程的技術(shù)洼滚。此外埂息,所有操作系統(tǒng)同樣為這些線程提供了管理和同步支持技潘。以下各節(jié)描述了一些你需要知道工作在OS X和iOS系統(tǒng)中線程的關(guān)鍵技術(shù)。
線程的層級(jí)
雖然線程的底層實(shí)現(xiàn)機(jī)制是Mach線程千康,但是你很少(如果有)在Mach線程上完成工作享幽。相反,你通常使用更方便的POSIX API或其衍生物拾弃。Mach線程實(shí)現(xiàn)方式能提供所有線程的基本功能值桩,包括搶占式的執(zhí)行模型和線程調(diào)度的能力,并且這兩部分是彼此獨(dú)立的豪椿。
表1-2 線程技術(shù)
名稱 | 描述 |
---|---|
Cocoa線程(Cocoa threads) | Cocoa使用NSThread來(lái)實(shí)現(xiàn)線程奔坟。同時(shí)也在已經(jīng)存在的線程上為NSObject提供了產(chǎn)生新線程(perform selector)的方法。 |
POSIX線程(POSIX threads) | POSIX線程使用基于C語(yǔ)言的接口創(chuàng)建線程搭盾。如果你編寫的不是Cocoa應(yīng)用咳秉,這會(huì)是創(chuàng)建線程的最佳方式。POSIX接口使用相對(duì)簡(jiǎn)單并且為線程的配置提供了足夠的靈活性鸯隅。 |
多進(jìn)程服務(wù)(Multiprocessing Services) | 多進(jìn)程服務(wù)是一種從老版本Mac OS過(guò)渡的基于傳統(tǒng)C語(yǔ)言的應(yīng)用技術(shù)澜建。這項(xiàng)技術(shù)僅用于OS X,并應(yīng)避免用于任何新的發(fā)展。相反炕舵,你應(yīng)該使用NSThread類或POSIX線程何之。 |
在應(yīng)用級(jí),所有線程的行為與其他平臺(tái)基本上是一樣的咽筋。在啟動(dòng)一個(gè)線程之后溶推,線程運(yùn)行在三個(gè)主狀態(tài)中的一個(gè):運(yùn)行、就緒或阻塞奸攻。如果一個(gè)線程當(dāng)前未運(yùn)行悼潭,它可以被阻塞以等待輸入,或者它已經(jīng)處于就緒狀態(tài)舞箍,但還沒(méi)有被調(diào)度執(zhí)行舰褪。該線程會(huì)持續(xù)在這些狀態(tài)中來(lái)回切換,直到它最后退出并切換到終止?fàn)顟B(tài)疏橄。
當(dāng)你創(chuàng)建新的線程時(shí)占拍,必須為其指定一個(gè)入口點(diǎn)函數(shù)(或Cocoa中的一個(gè)入口點(diǎn)方法)。這個(gè)入口點(diǎn)函數(shù)組合了你想在線程上運(yùn)行的代碼捎迫。當(dāng)函數(shù)返回時(shí)晃酒,或當(dāng)你顯示地終止線程時(shí),線程會(huì)永久停止并被系統(tǒng)回收窄绒。由于線程的創(chuàng)建對(duì)于內(nèi)存和時(shí)間消耗比較大贝次,因此建議你的輸入點(diǎn)函數(shù)里完成重要部分的工作或設(shè)置一個(gè)run loop以執(zhí)行重復(fù)性工作。
Run Loops
Run loop是一個(gè)在線程中異步地管理事件到達(dá)的基礎(chǔ)結(jié)構(gòu)彰导。它通過(guò)在線程上監(jiān)視一個(gè)或多個(gè)事件源來(lái)完成相應(yīng)工作蛔翅。當(dāng)事件到達(dá)時(shí),系統(tǒng)會(huì)喚醒線程并且在run loop中分發(fā)事件位谋,而run loop則將事件分發(fā)給你指定的回調(diào)處理代碼山析。如果沒(méi)有到達(dá)事件以及待處理事件時(shí),run loop會(huì)讓線程處于休眠狀態(tài)掏父。
你大可不必為創(chuàng)建的線程配置一個(gè)run loop笋轨,但你這樣做了,將會(huì)為用戶帶來(lái)更好的體驗(yàn)赊淑。Run loop使得花費(fèi)少量資源以創(chuàng)建長(zhǎng)生命周期的線程成為可能爵政。因?yàn)樗鼤?huì)讓線程在無(wú)事可做的時(shí)候休眠,它會(huì)停止輪詢工作以避免CPU資源的浪費(fèi)和電量的消耗陶缺。
要配置一個(gè)run loop钾挟,你需要做的只是啟動(dòng)你的線程,得到一個(gè)run loop的引用對(duì)象组哩,設(shè)置好事件回調(diào)處理代碼等龙,并且讓run loop啟動(dòng)处渣。操作系統(tǒng)已經(jīng)自動(dòng)地為你提供一個(gè)主線程的run loop回調(diào),然而你必須為你自己的線程配置run loop罐栈。
同步工具
線程編程的一個(gè)危險(xiǎn)在于多線程之間資源訪問(wèn)的沖突。如果多個(gè)線程嘗試在同一時(shí)間使用或修改同一資源泥畅,可能會(huì)出現(xiàn)問(wèn)題荠诬。緩解這個(gè)問(wèn)題的一個(gè)方法是完全消除共享資源,確保每個(gè)線程都有它自己的一組資源來(lái)操作位仁。當(dāng)不能保持完全獨(dú)立的資源時(shí)柑贞,你可能需要使用鎖、條件鎖聂抢、原子操作和其他技術(shù)來(lái)同步訪問(wèn)資源钧嘶。
鎖提供了一種強(qiáng)有力地保護(hù)代碼在同一線程的同一時(shí)間安全執(zhí)行的形式。最常見(jiàn)的鎖類型是互斥排他鎖琳疏,也被稱為互斥鎖有决。當(dāng)一個(gè)線程試圖獲取已被另一個(gè)線程持有的鎖,該線程會(huì)處于阻塞狀態(tài)直到鎖被其他線程釋放空盼。系統(tǒng)框架提供了互斥鎖的支持书幕,雖然它們都是基于相同的底層技術(shù)。此外揽趾,Cocoa提供了互斥鎖的幾個(gè)變種以支持不同類型的行為台汇,如遞歸鎖。
除了鎖篱瞎,系統(tǒng)還提供了條件鎖的支持苟呐,確保在你的應(yīng)用程序中進(jìn)行任務(wù)的適當(dāng)排序。條件鎖充當(dāng)一個(gè)“看門人”奔缠,阻塞一個(gè)給定的線程掠抬,直到它具有的條件滿足吼野。當(dāng)發(fā)生上述情況校哎,條件鎖會(huì)為線程放行,并允許其繼續(xù)執(zhí)行瞳步。POSIX層和Foundation Framework都為條件鎖提供了直接支持闷哆。(如果使用操作對(duì)象,則可以將操作對(duì)象間的依賴關(guān)系配置為執(zhí)行任務(wù)的順序单起,這與條件鎖提供的行為非常相似抱怔。)
盡管鎖和條件鎖在并發(fā)設(shè)計(jì)中極為常見(jiàn),原子操作是另一種方式來(lái)保護(hù)和同步訪問(wèn)數(shù)據(jù)嘀倒。原子操作是執(zhí)行標(biāo)量數(shù)據(jù)類型的數(shù)學(xué)或邏輯運(yùn)算時(shí)提供的一個(gè)輕量級(jí)的鎖替代方案屈留。原子操作使用特殊的硬件指令局冰,以確保在其他線程有機(jī)會(huì)訪問(wèn)之前完成對(duì)變量的修改。
線程間的通信
一個(gè)好的設(shè)計(jì)是最大限度地減少所需的通信量灌危,但在某些時(shí)候康二,線程之間的通信成為必要。(線程的職能是為你的應(yīng)用程序工作勇蝙,但如果工作的結(jié)果不被使用沫勿,那它的好處是什么?)線程可能需要處理新的工作請(qǐng)求味混,或者向應(yīng)用程序的主線程報(bào)告其進(jìn)展情況产雹。在這些情況下,你需要一種方式來(lái)從一個(gè)線程到另一個(gè)線程獲取信息翁锡。幸運(yùn)的是蔓挖,線程共享相同的進(jìn)程空間意味著你有很多可選的通信方式。
線程間的通信方式有很多馆衔,各有其優(yōu)缺點(diǎn)时甚。在OS X中,配置線程本地存儲(chǔ)作為最常見(jiàn)的通信機(jī)制哈踱。(除了消息隊(duì)列和Cocoa分布式對(duì)象荒适,這些技術(shù)在iOS也可用)。
表1-3 通信機(jī)制
名稱 | 描述 |
---|---|
直接通信(Direct messaging) | Cocoa應(yīng)用程序支持在其他線程上直接執(zhí)行選擇器的方式开镣。這種能力意味著一個(gè)線程基本上可以在任何其他線程上執(zhí)行方法刀诬。因?yàn)樗鼈兌荚谀繕?biāo)線程的上下文中執(zhí)行,消息會(huì)以這種方式自動(dòng)序列化該線程上邪财。 |
全局變量陕壹、共享內(nèi)存及對(duì)象(Global variables, shared memory, and objects) | 另一種簡(jiǎn)單的方法是使用全局變量、共享對(duì)象树埠、或共享內(nèi)存塊來(lái)傳遞信息糠馆。雖然共享變量是快速和簡(jiǎn)單的,但它們比直接通信更為脆弱怎憋。共享變量必須小心地由鎖或其他同步機(jī)制來(lái)確保代碼的正確性又碌。這樣做失敗的話可能導(dǎo)致競(jìng)態(tài)條件、數(shù)據(jù)損壞或應(yīng)用崩潰绊袋。 |
條件鎖(Conditions) | 條件鎖是一種可以控制線程執(zhí)行某個(gè)特定的代碼段的同步工具毕匀。你可以把條件鎖當(dāng)作“看門人”,只有當(dāng)規(guī)定的條件滿足時(shí)才讓線程執(zhí)行癌别。 |
Run loop源(Run loop sources) | 自定義run loop源可以讓你在線程上接收應(yīng)用具體的消息皂岔。因?yàn)樗鼈兪鞘录?qū)動(dòng)的,當(dāng)沒(méi)有任何事可以做時(shí)線程會(huì)自動(dòng)休眠展姐,這提高了線程的效率時(shí)躁垛。 |
端口與套接字(Ports and sockets) | 基于端口的通信是一種比較復(fù)雜的線程間的通信方式剖毯,但它也是一種非常可靠的技術(shù)教馆。更重要的是速兔,端口和套接字可用于與外部實(shí)體進(jìn)行通信,如其他進(jìn)程和服務(wù)活玲。為了提高效率涣狗,端口由run loop源實(shí)現(xiàn),所以當(dāng)沒(méi)有數(shù)據(jù)在端口等待時(shí)線程會(huì)休眠舒憾。 |
消息隊(duì)列(Message queues) | 傳統(tǒng)的多進(jìn)程服務(wù)定義了先進(jìn)先出(FIFO)隊(duì)列用于處理傳入和傳出的數(shù)據(jù)镀钓。雖然消息隊(duì)列簡(jiǎn)單且方便,但它們不像其他一些通信技術(shù)那樣高效镀迂。 |
Cocoa分布式對(duì)象(Cocoa distributed objects) | 分布式對(duì)象是一種基于端口通信的高等級(jí)實(shí)現(xiàn)的Cocoa技術(shù)丁溅。雖然該技術(shù)用于跨線程通信可行,但這樣做是非常不鼓勵(lì)的探遵,因?yàn)樗鼤?huì)導(dǎo)致資源過(guò)度開(kāi)銷窟赏。分布式對(duì)象更適合于與其他進(jìn)程進(jìn)行通信,其中進(jìn)程間的開(kāi)銷已經(jīng)很高了箱季。 |
線程開(kāi)發(fā)技巧
下面的部分提供指導(dǎo)涯穷,以幫助你用正確的代碼實(shí)現(xiàn)線程編程,并讓你的線程代碼實(shí)現(xiàn)更好的性能藏雏。正如任何性能優(yōu)化一樣拷况,你總應(yīng)該在收集相關(guān)的性能統(tǒng)計(jì)數(shù)據(jù)之前,期間和之后掘殴,再對(duì)代碼進(jìn)行優(yōu)化赚瘦。
避免顯式創(chuàng)建線程
手動(dòng)編寫線程創(chuàng)建代碼是冗長(zhǎng)的,而且有可能出現(xiàn)錯(cuò)誤奏寨,你應(yīng)該盡可能的避免它起意。OS X和iOS通過(guò)其他API提供隱式支持的并發(fā)。相較于自己創(chuàng)建線程病瞳,可以考慮使用異步API揽咕,GCD,或操作對(duì)象來(lái)完成工作仍源。這些技術(shù)為你的代碼在底層做線程相關(guān)的工作心褐,并保證其正確性。此外笼踩,GCD和操作對(duì)象能夠根據(jù)當(dāng)前系統(tǒng)的負(fù)載調(diào)節(jié)活動(dòng)線程的數(shù)量,這比你自己的代碼管理線程更加有效亡嫌。
保證線程合理地忙
如果你決定手動(dòng)創(chuàng)建和管理線程嚎于,請(qǐng)記住線程會(huì)占用寶貴的系統(tǒng)資源掘而。你應(yīng)該盡你最大的努力確保分配給線程的任務(wù)是合理的長(zhǎng)期且高效。同時(shí)于购,你不應(yīng)該害怕終止大部分時(shí)間都是處于空閑的線程袍睡。線程消耗不少的內(nèi)存,一些還是線性的(一定時(shí)間內(nèi)不能交換到磁盤)肋僧,所以釋放空閑線程不僅有助于減少應(yīng)用程序的內(nèi)存占用斑胜,也釋放更多的物理內(nèi)存供系統(tǒng)其他進(jìn)程使用。
注意:在終止空閑線程之前嫌吠,你應(yīng)該經(jīng)常記錄一組應(yīng)用程序當(dāng)前性能的基準(zhǔn)測(cè)量值止潘。嘗試更改后,使用額外的測(cè)量來(lái)驗(yàn)證這些更改實(shí)際上提高了多少性能辫诅,而不是直接終止線程凭戴。
避免線程共享數(shù)據(jù)
避免線程相關(guān)的資源沖突最最簡(jiǎn)單的方法是給每個(gè)線程在程序中它自己需要的任何數(shù)據(jù)的副本串稀。當(dāng)你最大限度地減少線程間的通信和資源沖突時(shí)锭汛,并行代碼就可以良好運(yùn)行了。
創(chuàng)建多線程應(yīng)用程序異常困難兴垦。即便你很小心地在代碼中所有正確的時(shí)刻鎖定共享數(shù)據(jù)肤视,代碼仍然可能處于不安全的狀態(tài)档痪。例如,你的代碼可以解決問(wèn)題假如在共享數(shù)據(jù)以特定的順序進(jìn)行修改邢滑。將代碼更改為基于事務(wù)的模型可能隨后會(huì)抵消多線程的性能優(yōu)勢(shì)钞它。消除資源爭(zhēng)用是首先要解決的問(wèn)題,一個(gè)簡(jiǎn)單的設(shè)計(jì)常常會(huì)帶來(lái)優(yōu)異的性能殊鞭。
線程與用戶界面
如果你的應(yīng)用程序具有圖形化的用戶界面遭垛,建議在應(yīng)用主線程接收用戶相關(guān)事件和界面更新。此方法有助于避免與處理用戶事件和繪圖窗口內(nèi)容相關(guān)聯(lián)的同步問(wèn)題操灿。某些框架如Cocoa锯仪,一般都要求這樣做,但即使對(duì)于那些不這樣要求趾盐,保持這種在主線程上的方式會(huì)有助于你簡(jiǎn)化用戶界面邏輯庶喜。
有幾個(gè)顯著的例外情況,在輔助線程上完成圖形化操作將會(huì)有巨大的性能優(yōu)勢(shì)救鲤。例如久窟,你可以使用輔助線程來(lái)創(chuàng)建和處理圖像并進(jìn)行其他圖像相關(guān)的計(jì)算,這可以大大提高性能本缠。如果你不確定某個(gè)特定的圖形化操作斥扛,可以計(jì)劃在主線程上進(jìn)行。
注意線程退出時(shí)行為
一個(gè)進(jìn)程會(huì)運(yùn)行直到所有的非分離(合并)線程退出丹锹。默認(rèn)情況下稀颁,只有應(yīng)用程序的主線程被創(chuàng)建為非分離的芬失,但你也可以用同樣的方式創(chuàng)建其他線程。當(dāng)用戶退出應(yīng)用程序時(shí)匾灶,立即終止所有分離線程被認(rèn)為是最恰當(dāng)?shù)男袨槔饫茫驗(yàn)?em>分離線程上的工作是可選的。如果你的應(yīng)用程序是使用后臺(tái)線程來(lái)保存數(shù)據(jù)到磁盤或做其他的關(guān)鍵工作阶女,你需要?jiǎng)?chuàng)建非分離線程以防止應(yīng)用程序退出時(shí)數(shù)據(jù)丟失颊糜。
創(chuàng)建非分離線程需要額外的工作。因?yàn)樽罡呒?jí)線程技術(shù)默認(rèn)不創(chuàng)建可連接線程秃踩,可以使用POSIX API來(lái)創(chuàng)建它衬鱼。此外,當(dāng)它們最終退出時(shí)吞瞪,必須添加代碼將其合并到主線程中馁启。
如果你正在編寫一個(gè)Cocoa應(yīng)用,你還可以使用applicationShouldTerminate:
委托回調(diào)方法在應(yīng)用完全終止前延遲終止芍秆。當(dāng)延遲終止時(shí)惯疙,應(yīng)用程序?qū)⒌鹊饺魏侮P(guān)鍵線程已經(jīng)完成了它們的任務(wù),然后才調(diào)用replyToApplicationShouldTerminate:
方法妖啥。
異常處理
異常處理機(jī)制依賴于當(dāng)前調(diào)用堆棧霉颠,以在異常拋出時(shí)執(zhí)行任何必要的清理。由于每個(gè)線程都有自己的調(diào)用堆棧荆虱,所以每個(gè)線程只負(fù)責(zé)捕捉自己的異常蒿偎。同時(shí)未能在輔助線程和主線程中捕獲異常則說(shuō)明該進(jìn)程終止了。你不能將未捕獲的異常拋給同一進(jìn)程中的不同線程怀读。
如果需要通知另一個(gè)線程(如主線程)在當(dāng)前線程中的異常情況诉位,你應(yīng)該捕獲異常并簡(jiǎn)單地發(fā)送消息到另一個(gè)線程以說(shuō)明發(fā)生了什么。根據(jù)你的需求菜枷,捕獲異常的線程可以等待指令繼續(xù)執(zhí)行(如果可能的話)苍糠,或者干脆退出。
注意:在Cocoa中啤誊,NSException對(duì)象作為一個(gè)獨(dú)立的對(duì)象被捕獲后可以在線程中傳遞岳瞭。
在某些情況下,異常處理回調(diào)會(huì)自動(dòng)創(chuàng)建蚊锹。例如瞳筏,@synchronized代碼塊在Objective-C中就包含隱式的異常處理回調(diào)。
干凈利落地終止線程
線程退出的最佳途徑是讓其自然地到達(dá)它的主入口路徑結(jié)束牡昆。盡管有立即終止線程的函數(shù)姚炕,但這些函數(shù)應(yīng)該只作為最后手段使用。不建議在線程達(dá)到了它的自然終點(diǎn)時(shí)提前終止線程。如果線程已分配內(nèi)存钻心,打開(kāi)了文件凄硼,或獲得其他類型的資源铅协,這樣做可能無(wú)法回收這些資源捷沸,導(dǎo)致內(nèi)存泄漏或其他潛在問(wèn)題。
庫(kù)的線程安全
應(yīng)用開(kāi)發(fā)者已經(jīng)掌握了應(yīng)用中是否使用多線程狐史,然而庫(kù)開(kāi)發(fā)人員卻不一定痒给。當(dāng)開(kāi)發(fā)庫(kù)時(shí),你必須假定調(diào)用應(yīng)用程序是多線程的骏全,或者可以隨時(shí)切換到多線程苍柏。因此,你應(yīng)該經(jīng)常為關(guān)鍵部分代碼上鎖姜贡。
對(duì)于庫(kù)開(kāi)發(fā)人員而言试吁,只有當(dāng)應(yīng)用程序成為多線程時(shí)才創(chuàng)建鎖是不明智的。如果你需要在某個(gè)時(shí)候鎖定代碼楼咳,在早期的庫(kù)中創(chuàng)建一個(gè)鎖對(duì)象熄捍,最好是在一個(gè)顯式調(diào)用庫(kù)中進(jìn)行初始化。雖然你也可以使用靜態(tài)庫(kù)的初始化函數(shù)來(lái)創(chuàng)建這樣的鎖母怜,但只有當(dāng)沒(méi)有其他方法時(shí)余耽,才可以嘗試這樣做。執(zhí)行一個(gè)初始化函數(shù)增加了加載庫(kù)所需的時(shí)間苹熏,并可能對(duì)性能產(chǎn)生不利影響碟贾。
注意:永遠(yuǎn)記住在你的庫(kù)中鎖定和解鎖操作的平衡。你還應(yīng)該記住為庫(kù)的數(shù)據(jù)上鎖轨域,而不是依賴于調(diào)用代碼來(lái)提供一個(gè)線程安全的環(huán)境袱耽。
如果你正在開(kāi)發(fā)一個(gè)Cocoa庫(kù),可以注冊(cè)一個(gè)接收NSWillBecomeMultiThreadedNotification的觀察者干发,以在應(yīng)用成為多線程時(shí)得到通知朱巨。然而你不應(yīng)該依賴于此通知,因?yàn)樗赡軙?huì)在你的庫(kù)代碼被調(diào)用之前就分發(fā)出了铐然。
線程管理
OS X和iOS中每個(gè)進(jìn)程(應(yīng)用程序)是由一個(gè)或多個(gè)線程組成蔬崩,每個(gè)線程通過(guò)代碼表示一個(gè)獨(dú)立的執(zhí)行路徑。每個(gè)應(yīng)用程序都以單個(gè)線程開(kāi)始搀暑,它運(yùn)行應(yīng)用程序的main函數(shù)沥阳。應(yīng)用程序可以產(chǎn)生附加的線程,每個(gè)線程都執(zhí)行特定功能的代碼自点。
當(dāng)應(yīng)用程序啟動(dòng)一個(gè)新線程桐罕,該線程在應(yīng)用程序的進(jìn)程空間成為一個(gè)獨(dú)立的實(shí)體。每一個(gè)線程都有自己的執(zhí)行堆棧,并由內(nèi)核單獨(dú)調(diào)度運(yùn)行功炮。一個(gè)線程可以與其他的線程和其他進(jìn)程進(jìn)行通信溅潜,執(zhí)行I/O操作,以及做其他你可能需要它做的事情薪伏。因?yàn)樗鼈兲幱谙嗤倪M(jìn)程空間內(nèi)滚澜,所有線程在應(yīng)用中共享相同的虛擬內(nèi)存空間,并具有和進(jìn)程相同的訪問(wèn)權(quán)限嫁怀。
本章概述了在OS X和iOS中的線程技術(shù)设捐,可隨著例子說(shuō)明如何在應(yīng)用中使用這些技術(shù)。
資源消耗
線程會(huì)在內(nèi)存使用和性能方面對(duì)你的程序(和系統(tǒng))產(chǎn)生消耗塘淑。每個(gè)線程都需要內(nèi)核內(nèi)存空間和程序內(nèi)存空間中的內(nèi)存分配萝招。管理和協(xié)調(diào)調(diào)度線程的核心結(jié)構(gòu)由線性內(nèi)存存儲(chǔ)在內(nèi)核中。線程的堆棿孓啵空間和每個(gè)線程數(shù)據(jù)存儲(chǔ)在程序的內(nèi)存空間中槐沼。當(dāng)你首次創(chuàng)建線程時(shí)這些結(jié)構(gòu)被創(chuàng)建和初始化,由于需要和內(nèi)核進(jìn)行交互這次資源消耗相對(duì)昂貴捌治。
表2-1量化了創(chuàng)建應(yīng)用程序中一個(gè)新用戶級(jí)線程的大致消耗岗钩。其中一些指標(biāo)是可配置的,比如為輔助線程分配的堆椌叩危空間的量凹嘲。創(chuàng)建一個(gè)線程的時(shí)間成本是一個(gè)粗略的近似,應(yīng)該只用于彼此間相對(duì)比較构韵。線程創(chuàng)建時(shí)間會(huì)根據(jù)處理器的負(fù)載周蹭、計(jì)算機(jī)的速度和可用的系統(tǒng)和程序存儲(chǔ)器的數(shù)量而發(fā)生變化。
表2-1 線程創(chuàng)建消耗
指標(biāo) | 近似消耗 | 描述 |
---|---|---|
內(nèi)核數(shù)據(jù)(Kernel data structures) | 大約1KB | 該部分內(nèi)存用于存儲(chǔ)線程基本數(shù)據(jù)結(jié)構(gòu)和屬性疲恢,且大部分屬于線性內(nèi)存因此它不能被交換到磁盤上去凶朗。 |
堆棧空間(Stack space) | 512KB(輔助線程)显拳、8MB(OS X主線程)棚愤、1MB(iOS主線程) | 允許的最小堆棧大小為16KB,輔助線程的堆棧大小是4KB的倍數(shù)杂数。在線程創(chuàng)建時(shí)宛畦,該內(nèi)存的空間被放置在你的進(jìn)程空間中,但與該內(nèi)存相關(guān)聯(lián)的實(shí)際頁(yè)面直到線程被需要時(shí)才創(chuàng)建揍移。 |
創(chuàng)建用時(shí)(Creation time) | 大約90微秒 | 這個(gè)值反映了初始調(diào)用創(chuàng)建線程和線程的切入點(diǎn)開(kāi)始執(zhí)行時(shí)之間的時(shí)間次和。測(cè)定數(shù)據(jù)基于英特爾的iMac/2 GHz雙核處理器/1 GB RAM/OS X 10.5,通過(guò)分析平均值和中位數(shù)的過(guò)程中產(chǎn)生的線程創(chuàng)建那伐。 |
注意:由于底層內(nèi)核的支持踏施,操作對(duì)象通呈幔可以更快地創(chuàng)建線程。并非每次都從頭開(kāi)始創(chuàng)建線程畅形,它們使用在內(nèi)核中已駐留的線程池以節(jié)省分配時(shí)間养距。
另一個(gè)需要考慮的是寫線程代碼的生產(chǎn)成本。設(shè)計(jì)一個(gè)線程應(yīng)用程序日熬,有時(shí)可能需要對(duì)你的應(yīng)用程序的數(shù)據(jù)結(jié)構(gòu)組織方式的根本變化棍厌。這些變化可能是必要的,以避免使用時(shí)同步碍遍,這本身就可以對(duì)設(shè)計(jì)簡(jiǎn)單的應(yīng)用產(chǎn)生時(shí)間成本消耗定铜。設(shè)計(jì)這些數(shù)據(jù)結(jié)構(gòu)阳液,并在線程代碼中調(diào)試問(wèn)題怕敬,會(huì)增加開(kāi)發(fā)一個(gè)線程應(yīng)用程序所需要的時(shí)間。如果你的線程花太多時(shí)間等待鎖或不做任何事帘皿,在運(yùn)行時(shí)會(huì)產(chǎn)生更大的問(wèn)題东跪。
線程創(chuàng)建
創(chuàng)建低級(jí)別線程相對(duì)簡(jiǎn)單。在所有情況下鹰溜,你必須有一個(gè)函數(shù)或方法來(lái)充當(dāng)線程的主要入口點(diǎn)虽填,并且必須使用一個(gè)可用的線程例程來(lái)啟動(dòng)你的線程。下面的部分顯示了更為常用的線程技術(shù)的基本創(chuàng)建過(guò)程曹动。使用這些技術(shù)創(chuàng)建的線程繼承了默認(rèn)的屬性集取決于所使用的技術(shù)斋日。
使用NSThread
有兩種使用NSThread來(lái)創(chuàng)建線程的方式:
- 使用類方法
detachNewThreadSelector:toTarget:withObject:
來(lái)產(chǎn)生新線程。 - 創(chuàng)建一個(gè)NSTread對(duì)象并調(diào)用其
start
方法墓陈。(iOS及OS X 10.5后支持)
兩種方式都會(huì)在應(yīng)用中創(chuàng)建一個(gè)分離線程恶守。分離線程意味著該線程的資源會(huì)被系統(tǒng)自動(dòng)回收,即便在線程存在的情況下贡必。這也意味著該線程上的代碼不能顯式地合并到其他線程上去兔港。
由于OS X所有版本均支持detachNewThreadSelector:toTarget:withObject:
方法,所以該方法在Cocoa線程應(yīng)用中十分常見(jiàn)仔拟。創(chuàng)建一個(gè)新的分離線程時(shí)衫樊,你僅需提供一個(gè)方法(具體為一個(gè)選擇器)作為線程執(zhí)行的切入點(diǎn),定義該方法的對(duì)象利花,以及任何你想在線程啟動(dòng)時(shí)傳遞的數(shù)據(jù)科侈。下面的代碼示例將展示使用該方法來(lái)為當(dāng)前對(duì)象的自定義方法完成線程的創(chuàng)建。
[NSThread detachNewThreadSelector:@selector(myThreadMainMethod:)
toTarget:self
withObject:nil];
在OS X 10.5之前炒事,主要使用NSThread類產(chǎn)生線程臀栈。盡管會(huì)得到一個(gè)NSThread對(duì)象和訪問(wèn)一些線程屬性,這只能在線程本身運(yùn)行之后才行羡洛。在OS X 10.5挂脑,支持添加創(chuàng)建NSThread對(duì)象沒(méi)有立即產(chǎn)生相應(yīng)的新線程藕漱。(iOS同樣支持)這使得在啟動(dòng)線程之前可以獲取和設(shè)置不同的線程屬性,也使得能使用該線程的引用對(duì)象來(lái)稍后啟動(dòng)線程崭闲。
在OS X 10.5及之后版本有一種初始化NSThread對(duì)象的簡(jiǎn)單方法肋联,即使用initWithTarget:selector:object:
方法。該方法和detachNewThreadSelector:toTarget:withObject:
方法一樣刁俭,可使用它來(lái)初始化一個(gè)新的NSThread實(shí)例橄仍。然而,它并不啟動(dòng)線程牍戚。要啟動(dòng)線程侮繁,需要顯式調(diào)用線程對(duì)象的start
方法,如下面的示例所示:
NSThread* myThread = [[NSThread alloc] initWithTarget:self
selector:@selector(myThreadMainMethod:)
object:nil];
[myThread start]; // 線程實(shí)際創(chuàng)建
注意:使用
initWithTarget:selector:object:
方法是子類化NSThread并重寫其main方法如孝。最好使用重寫版本的這個(gè)方法實(shí)現(xiàn)線程的主入口點(diǎn)宪哩。
如果有一個(gè)NSThread對(duì)象的線程正在運(yùn)行,應(yīng)用中絕大多數(shù)對(duì)象可以使用performSelector:onThread:withObject:waitUntilDone:
方法來(lái)向該線程發(fā)送消息第晰。OS X 10.5引入的在除主線程外上執(zhí)行選擇器的支持是線程之間一種便捷的通信方式锁孟。(iOS同樣支持)使用該技術(shù)發(fā)送的信息直接由其他線程處于正常run loop時(shí)處理。(當(dāng)然茁瘦,這并不意味著目標(biāo)線程必須運(yùn)行在自身的run loop中)品抽。當(dāng)以這種方式通信時(shí),仍然需要某種同步形式甜熔,但這樣做比設(shè)置線程間的通信端口更簡(jiǎn)單圆恤。
注意:雖然在線程間通信時(shí)偶爾使用這種方式還行,但在時(shí)間敏感或線程間通信頻繁時(shí)最好不要使用
performSelector:onThread:withObject:waitUntilDone:
方法腔稀。
使用POSIX線程
OS X和iOS支持使用基于C語(yǔ)言的POSIX線程API來(lái)創(chuàng)建線程盆昙。這項(xiàng)技術(shù)實(shí)際能夠被用于任何類型的應(yīng)用(包括Cocoa和Cocoa Touch應(yīng)用)以及對(duì)你編寫跨平臺(tái)的應(yīng)用大有裨益。POSIX中創(chuàng)建線程的入口叫做pthread_create
烧颖。
代碼2-1展示了用POSIX調(diào)用完成線程創(chuàng)建的兩個(gè)自定義函數(shù)弱左。LaunchThread
函數(shù)創(chuàng)建了一個(gè)主入口由PosixThreadMainRoutine
函數(shù)實(shí)現(xiàn)的線程。由于POSIX方式創(chuàng)建的線程默認(rèn)是可合并的炕淮,本例中創(chuàng)建了一個(gè)分離線程拆火。將線程標(biāo)記為可分離使得線程退出時(shí)其資源會(huì)迅速被系統(tǒng)回收。
代碼2-1 C語(yǔ)言線程創(chuàng)建
#include <assert.h>
#include <pthread.h>
void* PosixThreadMainRoutine(void* data)
{
// 完成某些工作
return NULL;
}
void LaunchThread()
{
// 使用POSIX例程創(chuàng)建線程
pthread_attr_t attr;
pthread_t posixThreadID;
int returnVal;
returnVal = pthread_attr_init(&attr);
assert(!returnVal);
returnVal = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
assert(!returnVal);
int threadError = pthread_create(&posixThreadID, &attr, &PosixThreadMainRoutine, NULL);
returnVal = pthread_attr_destroy(&attr);
assert(!returnVal);
if (threadError != 0)
{
// 記錄錯(cuò)誤
}
}
如果將上述代碼加入你的源文件并完成對(duì)LaunchThread
函數(shù)的調(diào)用涂圆,這會(huì)在你的應(yīng)用中創(chuàng)建一個(gè)新的分離線程们镜。顯然,使用該代碼創(chuàng)建的線程并不完成任何有意義的工作润歉。線程在啟動(dòng)之后幾乎很快就會(huì)退出模狭。為了使事情更加有趣,你可以在代碼中為PosixThreadMainRoutine
函數(shù)添加實(shí)質(zhì)性的工作踩衩。此外嚼鹉,你還可以在創(chuàng)建時(shí)向函數(shù)指針傳遞指針數(shù)據(jù)贩汉,該指針作為pthread_create
函數(shù)最后一個(gè)參數(shù)傳入。
為了讓新創(chuàng)建的線程與應(yīng)用的主線程交流信息锚赤,你必須在目標(biāo)線程間創(chuàng)建通信路徑匹舞。基于C語(yǔ)言的應(yīng)用程序线脚,有幾個(gè)線程間通信的方式赐稽,包括端口、條件鎖或共享內(nèi)存浑侥。對(duì)于長(zhǎng)生命周期的線程來(lái)講姊舵,你通常應(yīng)該設(shè)置某些線程內(nèi)部通信機(jī)制以使應(yīng)用的主線程能夠在應(yīng)用退出時(shí)檢測(cè)其他線程的狀態(tài)或者干凈地結(jié)束它們。
使用NSObject產(chǎn)生線程
在iOS及OS X 10.5之后寓落,所有對(duì)象都能夠產(chǎn)生新的線程并且將其用于執(zhí)行它們的方法括丁。performSelectorInBackground:withObject:
方法會(huì)創(chuàng)建新的分離線程并且使用具體的方法作為新線程的切入點(diǎn)。例如零如,如果有某個(gè)對(duì)象(用變量myObj
表示)以及對(duì)象有一個(gè)需要在后臺(tái)運(yùn)行的方法名叫doSomething
躏将,可以使用如下代碼來(lái)完成:
[myObj performSelectorInBackground:@selector(doSomething) withObject:nil];
這樣做的效果與調(diào)用NSThread的detachNewThreadSelector:toTarget:withObject:
方法,輔以傳遞當(dāng)前對(duì)象考蕾、選擇器加上參數(shù)對(duì)象的方式一樣。新的線程生成方式會(huì)立即以默認(rèn)配置生成線程并立即啟動(dòng)会宪。在選擇器內(nèi)部肖卧,你可以像配置其他的線程一樣的配置該線程。例如掸鹅,你可以按照需要?jiǎng)?chuàng)建一個(gè)自動(dòng)釋放池(如果你不使用垃圾回收機(jī)制)并在想要使用時(shí)配置該線程的run loop塞帐。
Cocoa應(yīng)用中使用POSIX線程
盡管NSThread是Cocoa應(yīng)用中?創(chuàng)建線程的主要接口,如果方便你反而盡可以使用POSIX線程巍沙。例如葵姥,你會(huì)在已經(jīng)使用POSIX線程的代碼基礎(chǔ)上繼續(xù)使用而不是重寫這部分代碼。如果你打算在Cocoa應(yīng)用中使用POSIX線程句携,你仍需清楚Cocoa與線程的交互以及遵循以下部分建議榔幸。
保護(hù)Cocoa框架
對(duì)于多線程應(yīng)用,Cocoa框架會(huì)使用鎖及其他同步機(jī)制來(lái)保證正確的行為矮嫉。為了防止鎖導(dǎo)致單線程情況下性能降低的問(wèn)題削咆,Cocoa使用NSThread產(chǎn)生新線程時(shí)并沒(méi)有創(chuàng)建鎖。如果你僅使用POSIX例程來(lái)創(chuàng)建線程蠢笋,Cocoa框架并沒(méi)有收到你的應(yīng)用變?yōu)槎嗑€程的通知拨齐。如此,涉及到Cocoa框架的操作將會(huì)變得不穩(wěn)定甚至導(dǎo)致應(yīng)用崩潰昨寞。
為了讓Cocoa知道你的應(yīng)用即將使用多線程瞻惋,你需要做的是使用NSThread產(chǎn)生一個(gè)線程并使其立即退出厦滤。線程的切入點(diǎn)里并不做任何工作。然而歼狼,僅僅這樣的行為就足以讓Cocoa在需要的地方加鎖馁害。
如果你不確定Cocoa是否認(rèn)為你的應(yīng)用處于多線程狀態(tài),你可以使用NSThread的isMultiThreaded
方法檢測(cè)蹂匹。
混用POSIX和Cocoa鎖
在同一應(yīng)用中混用POSIX和Cocoa鎖是安全的碘菜。Cocoa的鎖和條件對(duì)象本質(zhì)上是POSIX上的一層簡(jiǎn)單封裝。然而限寞,對(duì)于既定的鎖忍啸,你必須總是使用同一接口來(lái)創(chuàng)建和控制該鎖。換句話說(shuō)履植,你不能用Cocoa的NSLock對(duì)象控制一個(gè)由pthread_mutex_init
函數(shù)創(chuàng)建的鎖來(lái)完成互斥操作计雌,反之亦然。
線程配置
在線程創(chuàng)建完成之后玫霎,你也許想配置些不同的線程環(huán)境凿滤。下面章節(jié)將描述一些能夠做以及何時(shí)做出這些修改的建議。
配置線程堆棧大小
每一個(gè)新創(chuàng)建好的線程庶近,系統(tǒng)會(huì)在進(jìn)程空間中分配具體大小的內(nèi)存作為該線程的堆棧翁脆。堆棧管理著堆棧片以及線程聲明的任何本地變量。這部分為線程分配的內(nèi)存叫做線程消耗鼻种。
如果想改變給定線程的堆棧大小反番,在線程創(chuàng)建之前你必須這樣做。所有基于線程的技術(shù)都會(huì)提供一些方法來(lái)設(shè)置堆棧大小叉钥,盡管只允許在iOS和OS X 10.5及之后版本使用NSThread的方式來(lái)設(shè)置罢缸。表2-2列出了每個(gè)技術(shù)的不同配置選項(xiàng)。
表2-2 設(shè)置線程堆棧大小
技術(shù) | 選項(xiàng) |
---|---|
Cocoa | 在iOS和OS X 10.5及之后版本投队,創(chuàng)建并初始化NSThread對(duì)象(不使用detachNewThreadSelector:toTarget:withObject: 方法)枫疆。在調(diào)用線程對(duì)象的start 方法之前,使用setStackSize: 來(lái)指定堆棧大小敷鸦。 |
POSIX | 創(chuàng)建pthread_attr_t 結(jié)構(gòu)體并調(diào)用pthread_attr_setstacksize 函數(shù)來(lái)改變默認(rèn)堆棧大小息楔。最后將該屬性結(jié)構(gòu)體傳遞給pthread_create 函數(shù)以創(chuàng)建線程。 |
多進(jìn)程服務(wù)(Multiprocessing Services) | 在創(chuàng)建線程時(shí)傳遞合適的線程大小給MPCreateTask 函數(shù)轧膘。 |
配置線程級(jí)儲(chǔ)存
每個(gè)線程維護(hù)著一個(gè)在線程中可以訪問(wèn)的鍵值對(duì)字典钞螟。你可以使用該字典來(lái)存儲(chǔ)在整個(gè)線程執(zhí)行階段的數(shù)據(jù)。例如谎碍,你可以存儲(chǔ)與線程run loop交互的狀態(tài)信息鳞滨。
Cocoa和POSIX使用不同的方式存儲(chǔ)該線程字典,所以你不能混用這兩種技術(shù)蟆淀。只要在線程代碼中堅(jiān)持使用其中一種技術(shù)拯啦,后期的方式也應(yīng)該相同澡匪。在Cocoa中,可以使用NSThread的threadDictionary
方法獲取到一個(gè)NSMutableDictionary對(duì)象褒链,在里面可以隨意添加線程需要的鍵值唁情。在POSIX中,可以使用pthread_setspecific
和pthread_getspecific
函數(shù)對(duì)線程的鍵值進(jìn)行設(shè)置和獲取操作甫匹。
設(shè)置獨(dú)立線程狀態(tài)
大部分的高等級(jí)線程技術(shù)默認(rèn)會(huì)創(chuàng)建分離線程甸鸟。多數(shù)情況下,分離線程更受青睞的原因是當(dāng)線程周期結(jié)束后系統(tǒng)可以立即回收線程持有的數(shù)據(jù)兵迅。分離線程同樣不需要顯式地和應(yīng)用進(jìn)行交互抢韭。這意味著從線程中獲取的結(jié)果可以交由自己處理。相比較而言恍箭,系統(tǒng)不會(huì)回收合并線程的資源直到其他線程顯式地和該線程進(jìn)行合并時(shí)刻恭,此時(shí)進(jìn)程會(huì)阻塞線程以完成合并。
你可以將合并線程理解為子線程扯夭。盡管它們?nèi)宰鳛楠?dú)立的線程鳍贾,合并線程必須與其他線程合并其資源才能被系統(tǒng)回收。合并線程同樣提供線程間顯式傳遞數(shù)據(jù)的方式交洗。在其退出之前骑科,合并線程可以傳遞一個(gè)數(shù)據(jù)指針或者其他返回類型給pthread_exit
函數(shù),其他線程可以通過(guò)調(diào)用pthread_join
函數(shù)來(lái)獲得該數(shù)據(jù)藕筋。
注意:在應(yīng)用退出時(shí)纵散,分離線程會(huì)立即終止而合并線程卻不是。每個(gè)合并線程必須在進(jìn)程允許其退出前完成合并操作隐圾。因此合并線程常用于關(guān)鍵且不被打斷的工作,如保存數(shù)據(jù)到磁盤掰茶。
如果你想創(chuàng)建合并線程暇藏,你只能使用POSIX線程來(lái)完成該操作。POSIX默認(rèn)創(chuàng)建合并線程濒蒋。為了標(biāo)記線程是可分離或者可合并的盐碱,在創(chuàng)建線程前需使用pthread_attr_setdetachstate
函數(shù)修改其線程屬性。在線程開(kāi)始后沪伙,可以使用pthread_detach
函數(shù)將合并線程改變成分離線程瓮顽。
設(shè)置線程優(yōu)先級(jí)
任何新創(chuàng)建的線程都有一個(gè)默認(rèn)的優(yōu)先級(jí)與其關(guān)聯(lián)。內(nèi)核的調(diào)度算法會(huì)根據(jù)線程的優(yōu)先級(jí)來(lái)決定線程的運(yùn)行順序围橡,高優(yōu)先級(jí)的線程比低優(yōu)先級(jí)的線程更容易被調(diào)度執(zhí)行暖混。高優(yōu)先級(jí)并不保證線程具體的執(zhí)行時(shí)間,僅僅意味著它相對(duì)于低優(yōu)先級(jí)線程更容易被調(diào)度器選擇翁授。
注意:最好的建議是保持各自線程默認(rèn)的優(yōu)先級(jí)拣播。提高某些線程的優(yōu)先級(jí)也可能會(huì)增加一些低優(yōu)先級(jí)線程的饑餓程度晾咪。如果你的應(yīng)用存在一個(gè)高優(yōu)先級(jí)線程和一個(gè)低優(yōu)先級(jí)線程進(jìn)行交互,低優(yōu)先級(jí)線程的“饑餓”會(huì)阻塞其他線程并造成性能瓶頸贮配。
如果你想修改線程的優(yōu)先級(jí)谍倦,Cocoa和POSIX均提供了方法來(lái)完成該操作。對(duì)于Cocoa線程泪勒,可以使用NSThread的類方法setThreadPriority:
來(lái)設(shè)置當(dāng)前運(yùn)行線程的優(yōu)先級(jí)昼蛀。對(duì)于POSIX線程,可以使用pthread_setschedparam
函數(shù)圆存。
編寫線程入口
對(duì)于大多數(shù)情況叼旋,在OS X以及其他平臺(tái)上線程的入口部分結(jié)構(gòu)大致相同。你會(huì)初始化數(shù)據(jù)結(jié)構(gòu)辽剧,布置一些工作或者選擇性地配置run loop送淆,然后在線程代碼完成后做清理工作。取決于你的設(shè)計(jì)怕轿,你需要在線程的入口點(diǎn)做些額外的工作偷崩。
創(chuàng)建自動(dòng)釋放池
由Objective-C框架鏈接的應(yīng)用通常需要為其線程創(chuàng)建至少一個(gè)自動(dòng)釋放池。如果應(yīng)用使用管理內(nèi)存方式(MRC和ARC)撞羽,自動(dòng)釋放池會(huì)捕獲任何在線程中標(biāo)記為可自動(dòng)釋放的對(duì)象阐斜。
如果應(yīng)用使用垃圾回收(GC)而不是管理內(nèi)存,自動(dòng)釋放池并不是嚴(yán)格意義上的需要诀紊。自動(dòng)釋放池對(duì)于垃圾回收機(jī)制下的應(yīng)用并無(wú)害處谒出,且多數(shù)情況下它會(huì)被忽略。代碼模塊同時(shí)支持管理內(nèi)存和垃圾回收是能夠被允許的邻奠。在這種情況下笤喳,自動(dòng)釋放池必須支持管理內(nèi)存方式而在垃圾回收允許時(shí)會(huì)被忽略。
如果你的應(yīng)用使用管理內(nèi)存方式碌宴,創(chuàng)建一個(gè)自動(dòng)釋放池是作為線程入口點(diǎn)的首要任務(wù)杀狡。同理,銷毀自動(dòng)釋放池則是線程中最后需要完成的事情贰镣。自動(dòng)釋放池會(huì)確保需要自動(dòng)釋放的對(duì)象被捕獲呜象,盡管它直到線程退出才會(huì)釋放它們。代碼2-2展示了使用自動(dòng)釋放池的基本線程代碼結(jié)構(gòu)碑隆。
代碼2-2 定義線程入口點(diǎn)
- (void)myThreadMainRoutine
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level池
// 完成線程工作
[pool release]; // 釋放池中對(duì)象
}
由于top-level的自動(dòng)釋放池直到線程退出才釋放其中對(duì)象恭陡,長(zhǎng)時(shí)間運(yùn)行的線程需要?jiǎng)?chuàng)建額外的釋放池來(lái)更加頻繁地釋放對(duì)象。例如上煤,配置run loop的線程會(huì)在每一個(gè)run loop周期完成自動(dòng)釋放池的創(chuàng)建和釋放休玩。更加頻繁地釋放對(duì)象以防止應(yīng)用內(nèi)存消耗暴增。和任何性能相關(guān)的問(wèn)題一樣,你都應(yīng)該在實(shí)際測(cè)量代碼性能之后再正確地使用自動(dòng)釋放池哥捕。
創(chuàng)建異常處理回調(diào)
如果你的應(yīng)用要捕獲和處理異常牧抽,那么你的線程代碼應(yīng)該準(zhǔn)備好捕獲任何可能產(chǎn)生的異常情況。盡管處理異常的最佳地點(diǎn)是在其可能發(fā)生的地方遥赚,捕獲異常失敗會(huì)導(dǎo)致應(yīng)用的退出扬舒。在線程入口代碼中加入try/catch快可以讓你捕獲任何未知的異常并提供正確的處理方式。
在Xcode創(chuàng)建的項(xiàng)目中你可以使用C++或者Objective-C的風(fēng)格的異常處理方式凫佛。
創(chuàng)建Run Loop
編寫代碼時(shí)想運(yùn)行在單獨(dú)的線程上時(shí)讲坎,有兩種選擇。第一種選擇是為線程編寫一個(gè)盡可能長(zhǎng)的且很少中斷的任務(wù)愧薛,當(dāng)任務(wù)完成時(shí)線程自然會(huì)退出晨炕。第二種是將線程放進(jìn)一個(gè)run loop中以在請(qǐng)求到達(dá)時(shí)動(dòng)態(tài)地處理。第一種選擇不需要代碼中特殊的設(shè)置毫炉,你只需直接開(kāi)始你要完成的工作瓮栗。然而第二種選擇,需要對(duì)線程的run loop做額外設(shè)置。
OS X和iOS對(duì)每個(gè)線程的的run loop實(shí)現(xiàn)提供了內(nèi)建支持。應(yīng)用框架會(huì)自動(dòng)地為主線程開(kāi)啟run loop帜篇。如果你創(chuàng)建了一個(gè)輔助線程醉蚁,你必須配置run loop并且手動(dòng)啟動(dòng)它酿箭。
終止線程
退出一個(gè)線程的推薦方式是讓其正常地退出它的入口點(diǎn)。盡管Cocoa、POSIX以及多進(jìn)程服務(wù)提供了直接殺線程的方法,但不鼓勵(lì)使用這些方法缨历。殺死線程阻止了線程的自我清理功能。分配給線程的內(nèi)存可能會(huì)泄漏以及其他正在被線程使用的資源得不到正確的清理糙麦,隨之而來(lái)的是潛在的問(wèn)題發(fā)生辛孵。
如果你預(yù)先要在線程執(zhí)行的過(guò)程中終止線程,你應(yīng)該為線程設(shè)計(jì)一套響應(yīng)取消和退出信息的操作赡磅。對(duì)于長(zhǎng)周期操作觉吭,這會(huì)意味著周期性地停止工作以檢查消息是否到達(dá)。如果要求線程退出的消息到達(dá)仆邓,線程才有時(shí)間執(zhí)行清理工作并優(yōu)雅地退出;反之伴鳖,它會(huì)繼續(xù)回到工作中并等待下一次消息的到來(lái)节值。
使用run loop的輸入源可以響應(yīng)取消操作消息及其類似消息。代碼2-3展示了線程中主入口的相關(guān)操作榜聂。該示例代碼為run loop配置了一個(gè)接受其他線程可能發(fā)送消息的輸入源搞疗。在完成部分任務(wù)之后,線程會(huì)進(jìn)入run loop以查看輸入源中的信息是否到達(dá)须肆。如果沒(méi)有匿乃,run loop立即退出并進(jìn)入下一個(gè)工作周期桩皿。由于回調(diào)并不直接訪問(wèn)exitNow變量,退出條件通過(guò)線程的鍵值字典獲取幢炸。
代碼2-3 在長(zhǎng)周期任務(wù)中檢測(cè)退出條件
- (void)threadMainRoutine
{
BOOL moreWorkToDo = YES;
BOOL exitNow = NO;
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
// thread-local加入exitNow布爾類型變量
NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
[threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];
// 配置自定義輸入源
[self myInstallCustomInputSource];
while (moreWorkToDo && !exitNow)
{
// 完成工作
// 工作完成后修改moreWorkToDo標(biāo)志
// 如果輸入源并未到達(dá)則run loop超時(shí)直接運(yùn)行
[runLoop runUntilDate:[NSDate date]];
// 檢測(cè)輸入源回調(diào)并修改exitNow值
exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
}
}