[譯]線程編程指南(一)

本文選譯自《Threading Programming Guide》

導(dǎo)語

線程技術(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)建和管理線程的接口。

注意:如果你正在開發(fā)一個(gè)新的應(yīng)用,你可以選擇使用該技術(shù)作為并發(fā)操作的實(shí)現(xiàn)之一。假使你并未真正理解該技術(shù)對(duì)于實(shí)現(xiàn)多線程應(yīng)用的技術(shù)細(xì)節(jié),這里有一些簡化并發(fā)操作實(shí)現(xiàn)難度并提供性能更加優(yōu)越的技術(shù)方案可供選擇。獲取更多信息,請(qǐng)參見《Concurrency Programming Guide》。

本文結(jié)構(gòu)

  • 關(guān)于線程編程
  • 線程管理
  • Run Loops
  • 線程同步
  • 線程安全總結(jié)

關(guān)于線程編程

多年以來,計(jì)算機(jī)性能峰值在很大程度上受制于單個(gè)微處理器的計(jì)算機(jī)核心。當(dāng)單個(gè)處理器的速度開始達(dá)到它們的實(shí)際限制時(shí),芯片制造商切換到多核設(shè)計(jì),以達(dá)到讓計(jì)算機(jī)同時(shí)執(zhí)行多個(gè)任務(wù)的目的。雖然操作系統(tǒng)可以利用多核技術(shù)來執(zhí)行系統(tǒng)相關(guān)的任務(wù),然而你自己的應(yīng)用也可以通過線程技術(shù)來利用該技術(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è)線程開始和結(jié)束于程序的main函數(shù),并由一個(gè)接一個(gè)的不同方法或函數(shù)來實(shí)現(xiàn)程序的全部行為。相比之下,支持并發(fā)的程序可以啟動(dòng)一個(gè)線程,并按照需求增加線程以創(chuàng)建額外的執(zhí)行路徑。每一條新路徑都有獨(dú)立的自定義啟動(dòng)入口,在程序的main函數(shù)中獨(dú)立運(yùn)行代碼。多線程的程序具有兩個(gè)非常重要的潛在優(yōu)勢:

  • 多線程可以提高應(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ù)的問題,所以當(dāng)其中的一個(gè)計(jì)算任務(wù)需要花費(fèi)很長時(shí)間來完成時(shí)怎么辦?當(dāng)你的應(yīng)用忙于計(jì)算,卻停止響應(yīng)用戶事件和更新窗口時(shí)。如果這樣的情況長期持續(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)用性能問題的萬能藥。線程技術(shù)為我們開發(fā)帶來好處的同時(shí)也伴隨著隱患。程序中的多個(gè)執(zhí)行路徑會(huì)給代碼添加相當(dāng)數(shù)量的復(fù)雜度。每個(gè)線程與其他線程必須協(xié)調(diào)行動(dòng)以防止程序的狀態(tài)信息被破壞。因?yàn)樵趩蝹€(gè)應(yīng)用程序中各線程共享相同的內(nèi)存空間,它們可以訪問所有共享的數(shù)據(jù)。如果兩個(gè)線程試圖同時(shí)操縱共享數(shù)據(jù),一個(gè)線程可能會(huì)覆蓋其他線程的修改,導(dǎo)致數(shù)據(jù)破壞。即使有適當(dāng)?shù)谋Wo(hù)措施,你仍需要注意編譯器優(yōu)化為代碼引入微妙的(和不那么微妙的)錯(cuò)誤。

相關(guān)術(shù)語

在深入討論線程及其支持技術(shù)之前,必須定義一些基本術(shù)語。
如果你熟悉UNIX系統(tǒng),你會(huì)發(fā)現(xiàn)task是由本文檔中使用不同的。在UNIX系統(tǒng)中,術(shù)語task的使用有時(shí)指一個(gè)正在運(yùn)行的進(jìn)程。

本文采用以下術(shù)語:

  • 線程(thread)用來指代碼的一個(gè)單獨(dú)執(zhí)行路徑。
  • 進(jìn)程(process)用來指一個(gè)正在運(yùn)行的可執(zhí)行文件,可包含多個(gè)線程。
  • 任務(wù)(task)用來指需要進(jìn)行工作的抽象概念。

線程替代技術(shù)

自己創(chuàng)建線程的一個(gè)問題是它可能會(huì)給你的代碼帶來不確定性。因?yàn)榫€程是一個(gè)比較低級(jí)別且復(fù)雜的方式來支持應(yīng)用程序的并發(fā)性。如果你不完全理解選擇該方式的含義,你很容易地遭遇線程同步或開發(fā)時(shí)間問題,其嚴(yán)重程度可以從細(xì)微的行為變化到應(yīng)用的崩潰以及用戶數(shù)據(jù)的破壞。

另一個(gè)需要考慮的因素是應(yīng)用到底是否需要線程或并發(fā)。線程解決了在同一進(jìn)程中同時(shí)執(zhí)行多個(gè)代碼路徑的具體問題??赡苡邢壤闼龅墓ぷ鞑恍枰l(fā)。線程在你的進(jìn)程中引入巨大的開銷,無論是在內(nèi)存消耗和處理器時(shí)間方面。你可能會(huì)發(fā)現(xiàn),對(duì)于預(yù)定的任務(wù)這個(gè)開銷太大,或者說其他選擇更容易實(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ù)而不是線程管理。通過GCD,你定義需要執(zhí)行的任務(wù)并將其添加到一個(gè)工作隊(duì)列中,它會(huì)調(diào)度你的任務(wù)在一個(gè)適當(dāng)?shù)木€程上執(zhí)行。工作隊(duì)列會(huì)根據(jù)可用的處理器核心數(shù)量和當(dāng)前的任務(wù)負(fù)載來執(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)建自定義線程來執(zhí)行任務(wù)并返回結(jié)果給你。(實(shí)際的實(shí)現(xiàn)是不相關(guān)的,因?yàn)樗菑哪愕拇a中分離出來的),當(dāng)設(shè)計(jì)你的應(yīng)用時(shí),尋找提供異步行為的函數(shù),并考慮使用它們,而不是使用自定義線程上的等效同步函數(shù)。
定時(shí)器(Timers) 你可以用定時(shí)器在應(yīng)用程序的主線程上周期性地執(zhí)行一些太過瑣碎而不需要?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)程來計(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來實(shí)現(xiàn)線程。同時(shí)也在已經(jīng)存在的線程上為NSObject提供了產(chǎn)生新線程(perform selector)的方法。
POSIX線程(POSIX threads) POSIX線程使用基于C語言的接口創(chuàng)建線程。如果你編寫的不是Cocoa應(yīng)用,這會(huì)是創(chuàng)建線程的最佳方式。POSIX接口使用相對(duì)簡單并且為線程的配置提供了足夠的靈活性。
多進(jìn)程服務(wù)(Multiprocessing Services) 多進(jìn)程服務(wù)是一種從老版本Mac OS過渡的基于傳統(tǒng)C語言的應(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),但還沒有被調(diào)度執(zhí)行。該線程會(huì)持續(xù)在這些狀態(tà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)。它通過在線程上監(jiān)視一個(gè)或多個(gè)事件源來完成相應(yīng)工作。當(dāng)事件到達(dá)時(shí),系統(tǒng)會(huì)喚醒線程并且在run loop中分發(fā)事件,而run loop則將事件分發(fā)給你指定的回調(diào)處理代碼。如果沒有到達(dá)事件以及待處理事件時(shí),run loop會(huì)讓線程處于休眠狀態(tài)。

你大可不必為創(chuàng)建的線程配置一個(gè)run loop,但你這樣做了,將會(huì)為用戶帶來更好的體驗(yàn)。Run loop使得花費(fèi)少量資源以創(chuàng)建長生命周期的線程成為可能。因?yàn)樗鼤?huì)讓線程在無事可做的時(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)在于多線程之間資源訪問的沖突。如果多個(gè)線程嘗試在同一時(shí)間使用或修改同一資源,可能會(huì)出現(xiàn)問題。緩解這個(gè)問題的一個(gè)方法是完全消除共享資源,確保每個(gè)線程都有它自己的一組資源來操作。當(dāng)不能保持完全獨(dú)立的資源時(shí),你可能需要使用鎖、條件鎖、原子操作和其他技術(shù)來同步訪問資源。

鎖提供了一種強(qiáng)有力地保護(hù)代碼在同一線程的同一時(shí)間安全執(zhí)行的形式。最常見的鎖類型是互斥排他鎖,也被稱為互斥鎖。當(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ì)中極為常見,原子操作是另一種方式來保護(hù)和同步訪問數(shù)據(jù)。原子操作是執(zhí)行標(biāo)量數(shù)據(jù)類型的數(shù)學(xué)或邏輯運(yùn)算時(shí)提供的一個(gè)輕量級(jí)的鎖替代方案。原子操作使用特殊的硬件指令,以確保在其他線程有機(jī)會(huì)訪問之前完成對(duì)變量的修改。

線程間的通信

一個(gè)好的設(shè)計(jì)是最大限度地減少所需的通信量,但在某些時(shí)候,線程之間的通信成為必要。(線程的職能是為你的應(yīng)用程序工作,但如果工作的結(jié)果不被使用,那它的好處是什么?)線程可能需要處理新的工作請(qǐng)求,或者向應(yīng)用程序的主線程報(bào)告其進(jìn)展情況。在這些情況下,你需要一種方式來從一個(gè)線程到另一個(gè)線程獲取信息。幸運(yùn)的是,線程共享相同的進(jìn)程空間意味著你有很多可選的通信方式。

線程間的通信方式有很多,各有其優(yōu)缺點(diǎn)。在OS X中,配置線程本地存儲(chǔ)作為最常見的通信機(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) 另一種簡單的方法是使用全局變量、共享對(duì)象、或共享內(nèi)存塊來傳遞信息。雖然共享變量是快速和簡單的,但它們比直接通信更為脆弱。共享變量必須小心地由鎖或其他同步機(jī)制來確保代碼的正確性。這樣做失敗的話可能導(dǎo)致競態(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)沒有任何事可以做時(shí)線程會(huì)自動(dòng)休眠,這提高了線程的效率時(shí)。
端口與套接字(Ports and sockets) 基于端口的通信是一種比較復(fù)雜的線程間的通信方式,但它也是一種非??煽康募夹g(shù)。更重要的是,端口和套接字可用于與外部實(shí)體進(jìn)行通信,如其他進(jìn)程和服務(wù)。為了提高效率,端口由run loop源實(shí)現(xiàn),所以當(dāng)沒有數(shù)據(jù)在端口等待時(shí)線程會(huì)休眠。
消息隊(duì)列(Message queues) 傳統(tǒng)的多進(jìn)程服務(wù)定義了先進(jìn)先出(FIFO)隊(duì)列用于處理傳入和傳出的數(shù)據(jù)。雖然消息隊(duì)列簡單且方便,但它們不像其他一些通信技術(shù)那樣高效。
Cocoa分布式對(duì)象(Cocoa distributed objects) 分布式對(duì)象是一種基于端口通信的高等級(jí)實(shí)現(xiàn)的Cocoa技術(shù)。雖然該技術(shù)用于跨線程通信可行,但這樣做是非常不鼓勵(lì)的,因?yàn)樗鼤?huì)導(dǎo)致資源過度開銷。分布式對(duì)象更適合于與其他進(jìn)程進(jìn)行通信,其中進(jìn)程間的開銷已經(jīng)很高了。

線程開發(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)建代碼是冗長的,而且有可能出現(xiàn)錯(cuò)誤,你應(yīng)該盡可能的避免它。OS X和iOS通過其他API提供隱式支持的并發(fā)。相較于自己創(chuàng)建線程,可以考慮使用異步API,GCD,或操作對(duì)象來完成工作。這些技術(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ù)是合理的長期且高效。同時(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)測量值。嘗試更改后,使用額外的測量來驗(yàn)證這些更改實(shí)際上提高了多少性能,而不是直接終止線程。

避免線程共享數(shù)據(jù)

避免線程相關(guān)的資源沖突最最簡單的方法是給每個(gè)線程在程序中它自己需要的任何數(shù)據(jù)的副本。當(dāng)你最大限度地減少線程間的通信和資源沖突時(shí),并行代碼就可以良好運(yùn)行了。

創(chuàng)建多線程應(yīng)用程序異常困難。即便你很小心地在代碼中所有正確的時(shí)刻鎖定共享數(shù)據(jù),代碼仍然可能處于不安全的狀態(tài)。例如,你的代碼可以解決問題假如在共享數(shù)據(jù)以特定的順序進(jìn)行修改。將代碼更改為基于事務(wù)的模型可能隨后會(huì)抵消多線程的性能優(yōu)勢。消除資源爭用是首先要解決的問題,一個(gè)簡單的設(shè)計(jì)常常會(huì)帶來優(yōu)異的性能。

線程與用戶界面

如果你的應(yīng)用程序具有圖形化的用戶界面,建議在應(yīng)用主線程接收用戶相關(guān)事件和界面更新。此方法有助于避免與處理用戶事件和繪圖窗口內(nèi)容相關(guān)聯(lián)的同步問題。某些框架如Cocoa,一般都要求這樣做,但即使對(duì)于那些不這樣要求,保持這種在主線程上的方式會(huì)有助于你簡化用戶界面邏輯。

有幾個(gè)顯著的例外情況,在輔助線程上完成圖形化操作將會(huì)有巨大的性能優(yōu)勢。例如,你可以使用輔助線程來創(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)線程來保存數(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來創(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í)未能在輔助線程和主線程中捕獲異常則說明該進(jìn)程終止了。你不能將未捕獲的異常拋給同一進(jìn)程中的不同線程。

如果需要通知另一個(gè)線程(如主線程)在當(dāng)前線程中的異常情況,你應(yīng)該捕獲異常并簡單地發(fā)送消息到另一個(gè)線程以說明發(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)存,打開了文件,或獲得其他類型的資源,這樣做可能無法回收這些資源,導(dǎo)致內(nèi)存泄漏或其他潛在問題。

庫的線程安全

應(yīng)用開發(fā)者已經(jīng)掌握了應(yīng)用中是否使用多線程,然而庫開發(fā)人員卻不一定。當(dāng)開發(fā)庫時(shí),你必須假定調(diào)用應(yīng)用程序是多線程的,或者可以隨時(shí)切換到多線程。因此,你應(yīng)該經(jīng)常為關(guān)鍵部分代碼上鎖。

對(duì)于庫開發(fā)人員而言,只有當(dāng)應(yīng)用程序成為多線程時(shí)才創(chuàng)建鎖是不明智的。如果你需要在某個(gè)時(shí)候鎖定代碼,在早期的庫中創(chuàng)建一個(gè)鎖對(duì)象,最好是在一個(gè)顯式調(diào)用庫中進(jìn)行初始化。雖然你也可以使用靜態(tài)庫的初始化函數(shù)來創(chuàng)建這樣的鎖,但只有當(dāng)沒有其他方法時(shí),才可以嘗試這樣做。執(zhí)行一個(gè)初始化函數(shù)增加了加載庫所需的時(shí)間,并可能對(duì)性能產(chǎn)生不利影響。

注意:永遠(yuǎn)記住在你的庫中鎖定解鎖操作的平衡。你還應(yīng)該記住為庫的數(shù)據(jù)上鎖,而不是依賴于調(diào)用代碼來提供一個(gè)線程安全的環(huán)境。

如果你正在開發(fā)一個(gè)Cocoa庫,可以注冊(cè)一個(gè)接收NSWillBecomeMultiThreadedNotification的觀察者,以在應(yīng)用成為多線程時(shí)得到通知。然而你不應(yīng)該依賴于此通知,因?yàn)樗赡軙?huì)在你的庫代碼被調(diào)用之前就分發(fā)出了。

線程管理

OS X和iOS中每個(gè)進(jìn)程(應(yīng)用程序)是由一個(gè)或多個(gè)線程組成,每個(gè)線程通過代碼表示一個(gè)獨(dú)立的執(zhí)行路徑。每個(gè)應(yīng)用程序都以單個(gè)線程開始,它運(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)程相同的訪問權(quán)限。

本章概述了在OS X和iOS中的線程技術(shù),可隨著例子說明如何在應(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和每個(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)是可配置的,比如為輔助線程分配的堆??臻g的量。創(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í)際頁面直到線程被需要時(shí)才創(chuàng)建。
創(chuàng)建用時(shí)(Creation time) 大約90微秒 這個(gè)值反映了初始調(diào)用創(chuàng)建線程和線程的切入點(diǎn)開始執(zhí)行時(shí)之間的時(shí)間。測定數(shù)據(jù)基于英特爾的iMac/2 GHz雙核處理器/1 GB RAM/OS X 10.5,通過分析平均值和中位數(shù)的過程中產(chǎn)生的線程創(chuàng)建。

注意:由于底層內(nèi)核的支持,操作對(duì)象通??梢愿斓貏?chuàng)建線程。并非每次都從頭開始創(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ì)簡單的應(yīng)用產(chǎn)生時(shí)間成本消耗。設(shè)計(jì)這些數(shù)據(jù)結(jié)構(gòu),并在線程代碼中調(diào)試問題,會(huì)增加開發(fā)一個(gè)線程應(yīng)用程序所需要的時(shí)間。如果你的線程花太多時(shí)間等待鎖或不做任何事,在運(yùn)行時(shí)會(huì)產(chǎn)生更大的問題。

線程創(chuàng)建

創(chuàng)建低級(jí)別線程相對(duì)簡單。在所有情況下,你必須有一個(gè)函數(shù)或方法來充當(dāng)線程的主要入口點(diǎn),并且必須使用一個(gè)可用的線程例程來啟動(dòng)你的線程。下面的部分顯示了更為常用的線程技術(shù)的基本創(chuàng)建過程。使用這些技術(shù)創(chuàng)建的線程繼承了默認(rèn)的屬性集取決于所使用的技術(shù)。

使用NSThread

有兩種使用NSThread來創(chuàng)建線程的方式:

  • 使用類方法detachNewThreadSelector:toTarget:withObject:來產(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)用中十分常見。創(chuàng)建一個(gè)新的分離線程時(shí),你僅需提供一個(gè)方法(具體為一個(gè)選擇器)作為線程執(zhí)行的切入點(diǎn),定義該方法的對(duì)象,以及任何你想在線程啟動(dòng)時(shí)傳遞的數(shù)據(jù)。下面的代碼示例將展示使用該方法來為當(dāng)前對(duì)象的自定義方法完成線程的創(chuàng)建。

[NSThread detachNewThreadSelector:@selector(myThreadMainMethod:)
                         toTarget:self
                       withObject:nil];

在OS X 10.5之前,主要使用NSThread類產(chǎn)生線程。盡管會(huì)得到一個(gè)NSThread對(duì)象和訪問一些線程屬性,這只能在線程本身運(yùn)行之后才行。在OS X 10.5,支持添加創(chuàng)建NSThread對(duì)象沒有立即產(chǎn)生相應(yīng)的新線程。(iOS同樣支持)這使得在啟動(dòng)線程之前可以獲取和設(shè)置不同的線程屬性,也使得能使用該線程的引用對(duì)象來稍后啟動(dòng)線程。

在OS X 10.5及之后版本有一種初始化NSThread對(duì)象的簡單方法,即使用initWithTarget:selector:object:方法。該方法和detachNewThreadSelector:toTarget:withObject:方法一樣,可使用它來初始化一個(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:方法來向該線程發(fā)送消息。OS X 10.5引入的在除主線程外上執(zhí)行選擇器的支持是線程之間一種便捷的通信方式。(iOS同樣支持)使用該技術(shù)發(fā)送的信息直接由其他線程處于正常run loop時(shí)處理。(當(dāng)然,這并不意味著目標(biāo)線程必須運(yùn)行在自身的run loop中)。當(dāng)以這種方式通信時(shí),仍然需要某種同步形式,但這樣做比設(shè)置線程間的通信端口更簡單。

注意:雖然在線程間通信時(shí)偶爾使用這種方式還行,但在時(shí)間敏感或線程間通信頻繁時(shí)最好不要使用performSelector:onThread:withObject:waitUntilDone:方法。

使用POSIX線程

OS X和iOS支持使用基于C語言的POSIX線程API來創(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語言線程創(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)建通信路徑?;贑語言的應(yīng)用程序,有幾個(gè)線程間通信的方式,包括端口、條件鎖或共享內(nèi)存。對(duì)于長生命周期的線程來講,你通常應(yīng)該設(shè)置某些線程內(nèi)部通信機(jī)制以使應(yīng)用的主線程能夠在應(yīng)用退出時(shí)檢測其他線程的狀態(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,可以使用如下代碼來完成:

[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ī)制來保證正確的行為。為了防止鎖導(dǎo)致單線程情況下性能降低的問題,Cocoa使用NSThread產(chǎn)生新線程時(shí)并沒有創(chuàng)建鎖。如果你僅使用POSIX例程來創(chuàng)建線程,Cocoa框架并沒有收到你的應(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方法檢測。

混用POSIX和Cocoa鎖

在同一應(yīng)用中混用POSIX和Cocoa鎖是安全的。Cocoa的鎖和條件對(duì)象本質(zhì)上是POSIX上的一層簡單封裝。然而,對(duì)于既定的鎖,你必須總是使用同一接口來創(chuàng)建和控制該鎖。換句話說,你不能用Cocoa的NSLock對(duì)象控制一個(gè)由pthread_mutex_init函數(shù)創(chuàng)建的鎖來完成互斥操作,反之亦然。

線程配置

在線程創(chuàng)建完成之后,你也許想配置些不同的線程環(huán)境。下面章節(jié)將描述一些能夠做以及何時(shí)做出這些修改的建議。

配置線程堆棧大小

每一個(gè)新創(chuàng)建好的線程,系統(tǒng)會(huì)在進(jìn)程空間中分配具體大小的內(nèi)存作為該線程的堆棧。堆棧管理著堆棧片以及線程聲明的任何本地變量。這部分為線程分配的內(nèi)存叫做線程消耗。

如果想改變給定線程的堆棧大小,在線程創(chuàng)建之前你必須這樣做。所有基于線程的技術(shù)都會(huì)提供一些方法來設(shè)置堆棧大小,盡管只允許在iOS和OS X 10.5及之后版本使用NSThread的方式來設(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:來指定堆棧大小。
POSIX 創(chuàng)建pthread_attr_t結(jié)構(gòu)體并調(diào)用pthread_attr_setstacksize函數(shù)來改變默認(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è)在線程中可以訪問的鍵值對(duì)字典。你可以使用該字典來存儲(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_setspecificpthread_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ù),其他線程可以通過調(diào)用pthread_join函數(shù)來獲得該數(shù)據(jù)。

注意:在應(yīng)用退出時(shí),分離線程會(huì)立即終止而合并線程卻不是。每個(gè)合并線程必須在進(jìn)程允許其退出前完成合并操作。因此合并線程常用于關(guān)鍵且不被打斷的工作,如保存數(shù)據(jù)到磁盤。

如果你想創(chuàng)建合并線程,你只能使用POSIX線程來完成該操作。POSIX默認(rèn)創(chuàng)建合并線程。為了標(biāo)記線程是可分離或者可合并的,在創(chuàng)建線程前需使用pthread_attr_setdetachstate函數(shù)修改其線程屬性。在線程開始后,可以使用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í)來決定線程的運(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均提供了方法來完成該操作。對(duì)于Cocoa線程,可以使用NSThread的類方法setThreadPriority:來設(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)用并無害處,且多數(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ì)象,長時(shí)間運(yùn)行的線程需要?jiǎng)?chuàng)建額外的釋放池來更加頻繁地釋放對(duì)象。例如,配置run loop的線程會(huì)在每一個(gè)run loop周期完成自動(dòng)釋放池的創(chuàng)建和釋放。更加頻繁地釋放對(duì)象以防止應(yīng)用內(nèi)存消耗暴增。和任何性能相關(guān)的問題一樣,你都應(yīng)該在實(shí)際測量代碼性能之后再正確地使用自動(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è)盡可能長的且很少中斷的任務(wù),當(dāng)任務(wù)完成時(shí)線程自然會(huì)退出。第二種是將線程放進(jìn)一個(gè)run loop中以在請(qǐng)求到達(dá)時(shí)動(dòng)態(tài)地處理。第一種選擇不需要代碼中特殊的設(shè)置,你只需直接開始你要完成的工作。然而第二種選擇,需要對(duì)線程的run loop做額外設(shè)置。

OS X和iOS對(duì)每個(gè)線程的的run loop實(shí)現(xiàn)提供了內(nèi)建支持。應(yīng)用框架會(huì)自動(dòng)地為主線程開啟run loop。如果你創(chuàng)建了一個(gè)輔助線程,你必須配置run loop并且手動(dòng)啟動(dòng)它。

終止線程

退出一個(gè)線程的推薦方式是讓其正常地退出它的入口點(diǎn)。盡管Cocoa、POSIX以及多進(jìn)程服務(wù)提供了直接殺線程的方法,但不鼓勵(lì)使用這些方法。殺死線程阻止了線程的自我清理功能。分配給線程的內(nèi)存可能會(huì)泄漏以及其他正在被線程使用的資源得不到正確的清理,隨之而來的是潛在的問題發(fā)生。

如果你預(yù)先要在線程執(zhí)行的過程中終止線程,你應(yīng)該為線程設(shè)計(jì)一套響應(yīng)取消和退出信息的操作。對(duì)于長周期操作,這會(huì)意味著周期性地停止工作以檢查消息是否到達(dá)。如果要求線程退出的消息到達(dá),線程才有時(shí)間執(zhí)行清理工作并優(yōu)雅地退出;反之,它會(huì)繼續(xù)回到工作中并等待下一次消息的到來。

使用run loop的輸入源可以響應(yīng)取消操作消息及其類似消息。代碼2-3展示了線程中主入口的相關(guān)操作。該示例代碼為run loop配置了一個(gè)接受其他線程可能發(fā)送消息的輸入源。在完成部分任務(wù)之后,線程會(huì)進(jìn)入run loop以查看輸入源中的信息是否到達(dá)。如果沒有,run loop立即退出并進(jìn)入下一個(gè)工作周期。由于回調(diào)并不直接訪問exitNow變量,退出條件通過線程的鍵值字典獲取。

代碼2-3 在長周期任務(wù)中檢測退出條件

- (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]];

        // 檢測輸入源回調(diào)并修改exitNow值
        exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 由于文章長度限制,本文作為[譯]線程編程指南(一)后續(xù)部分。 Run Loops Run loop是與線程相關(guān)的基...
    巧巧的二表哥閱讀 1,262評(píng)論 0 5
  • 這是一篇對(duì)Run Loop開發(fā)文檔《Threading Program Guide:Run Loops》的翻譯,來...
    鴻雁長飛光不度閱讀 3,833評(píng)論 3 29
  • 由于文章長度限制,本文作為[譯]線程編程指南(二)后續(xù)部分。 線程安全技巧 同步工具是保證代碼線程安全的有效方式,...
    巧巧的二表哥閱讀 1,409評(píng)論 0 4
  • 本文將從以下幾個(gè)部分來介紹多線程。 第一部分介紹多線程的基本原理。 第二部分介紹Run loop。 第三部分介紹多...
    曲年閱讀 1,339評(píng)論 2 14
  • 引用自多線程編程指南應(yīng)用程序里面多個(gè)線程的存在引發(fā)了多個(gè)執(zhí)行線程安全訪問資源的潛在問題。兩個(gè)線程同時(shí)修改同一資源有...
    Mitchell閱讀 2,108評(píng)論 1 7

友情鏈接更多精彩內(nèi)容