OS X 或iOS中的每個(gè)進(jìn)程(應(yīng)用程序)由一個(gè)或者多個(gè)線程組成,每個(gè)線程應(yīng)用程序的代碼表示單個(gè)執(zhí)行路徑。
每個(gè)應(yīng)用程序都從單個(gè)線程開(kāi)始,它運(yùn)行應(yīng)用程序的main功能。 應(yīng)用程序可以生成額外的線程,每個(gè)線程都執(zhí)行特定功能的代碼。
當(dāng)應(yīng)用程序產(chǎn)生一個(gè)新線程時(shí),該線程將成為應(yīng)用程序進(jìn)程空間內(nèi)的獨(dú)立實(shí)體。 每個(gè)線程都有自己的執(zhí)行堆棧,并由內(nèi)核分別安排運(yùn)行。 線程可以與其他線程和其他進(jìn)程進(jìn)行通信,執(zhí)行I / O操作,還可以執(zhí)行任何您可能需要執(zhí)行的操作。 因?yàn)樗鼈冊(cè)谙嗤倪M(jìn)程空間內(nèi),所以單個(gè)應(yīng)用程序中的所有線程共享相同的虛擬內(nèi)存空間,并具有與進(jìn)程本身相同的訪問(wèn)權(quán)限。
本章概述了OS X和iOS中可用的線程技術(shù),以及如何在應(yīng)用程序中使用這些技術(shù)的示例。
線程成本
線程在內(nèi)存使用和性能方面對(duì)您的程序(和系統(tǒng))有實(shí)際的成本。 每個(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í),大多數(shù)這些結(jié)構(gòu)都將被創(chuàng)建和初始化 - 由于與內(nèi)核所需的交互,這個(gè)進(jìn)程可能相對(duì)昂貴。
表1量化了在應(yīng)用程序中創(chuàng)建新的用戶(hù)級(jí)線程所需的大概成本。 這些成本中的一些是可配置的,例如為次要線程分配的堆??臻g量。 創(chuàng)建線程的時(shí)間成本是一個(gè)粗略的近似值,只能用于相互比較。 線程創(chuàng)建時(shí)間可能因處理器負(fù)載,計(jì)算機(jī)速度以及可用系統(tǒng)和程序存儲(chǔ)器的數(shù)量而異。
表1
項(xiàng)目 | 大概花費(fèi) | 描述 |
---|---|---|
內(nèi)核數(shù)據(jù)結(jié)構(gòu) | 大約1 KB | 該內(nèi)存用于存儲(chǔ)線程數(shù)據(jù)結(jié)構(gòu)和屬性,其中大部分被分配為有線內(nèi)存,因此不能被分頁(yè)到磁盤(pán)。 |
堆??臻g | 512 KB(二級(jí)線程)8 MB(OS X主線程)1 MB(iOS主線程) | 輔助線程的最小允許堆棧大小為16 KB,堆棧大小必須為4 KB的倍數(shù)。 這個(gè)內(nèi)存的空間在線程創(chuàng)建時(shí)被放在你的進(jìn)程空間中,但是在需要的時(shí)候才會(huì)創(chuàng)建與該內(nèi)存關(guān)聯(lián)的實(shí)際頁(yè)面。 |
創(chuàng)建時(shí)間 | 大約90微秒 | 此值反映了初始調(diào)用創(chuàng)建線程與線程入口點(diǎn)例程開(kāi)始執(zhí)行的時(shí)間。 這些數(shù)字是通過(guò)分析在基于Intel的iMac上使用2 GHz Core Duo處理器和1 GB RAM運(yùn)行OS X v10.5的線程創(chuàng)建過(guò)程中生成的平均值和中值。 |
注意:由于它們的底層內(nèi)核支持,操作對(duì)象通常可以更快地創(chuàng)建線程。 他們每次都從頭開(kāi)始創(chuàng)建線程,而是使用已經(jīng)駐留在內(nèi)核中的線程池來(lái)節(jié)省分配時(shí)間。
編寫(xiě)線程代碼時(shí)需要考慮的另一個(gè)代價(jià)是生產(chǎn)成本。 設(shè)計(jì)線程應(yīng)用程序有時(shí)可能需要對(duì)組織應(yīng)用程序數(shù)據(jù)結(jié)構(gòu)的方式進(jìn)行根本性更改。 進(jìn)行這些更改可能是必要的,以避免使用同步,這本身可能對(duì)設(shè)計(jì)不當(dāng)?shù)膽?yīng)用程序造成巨大的性能損失。 設(shè)計(jì)這些數(shù)據(jù)結(jié)構(gòu)和線程代碼中的調(diào)試問(wèn)題,可以增加開(kāi)發(fā)線程應(yīng)用程序所花費(fèi)的時(shí)間。 避免這些費(fèi)用可能會(huì)在運(yùn)行時(shí)產(chǎn)生更大的問(wèn)題,但是,如果你的線程花費(fèi)太多時(shí)間等待鎖或什么都不做。
創(chuàng)建線程
創(chuàng)建低級(jí)線程比較簡(jiǎn)單。 在所有情況下,您必須具有一個(gè)函數(shù)或方法作為線程的主要入口點(diǎn),并且您必須使用一個(gè)可用的線程例程來(lái)啟動(dòng)您的線程。 以下部分顯示了更常用的線程技術(shù)的基本創(chuàng)建過(guò)程。 使用這些技術(shù)創(chuàng)建的線程將繼承一組默認(rèn)屬性,由您使用的技術(shù)確定。
使用NSThread
使用NSthread類(lèi)創(chuàng)建一個(gè)線程有兩種方法:
使用
datachNewThreadSelector:toTarget:withoutObject:
方法來(lái)生成新線程。創(chuàng)建一個(gè)新的Thread 對(duì)象并調(diào)用其start方法。
這兩種技術(shù)在您的應(yīng)用程序中創(chuàng)建一個(gè)分離的線程。 分離的線程意味著當(dāng)線程退出時(shí),線程的資源將被系統(tǒng)自動(dòng)回收。 這也意味著您的代碼不必在以后明確加入該線程。
因?yàn)樵谒邪姹镜腛S X中都支持detachNewThreadSelector:toTarget:withObject: method,所以在現(xiàn)有Cocoa應(yīng)用程序中通常會(huì)使用線程。 要分離一個(gè)新線程,只需提供要用作線程入口點(diǎn)的方法(指定為選擇器)的名稱(chēng),定義該方法的對(duì)象以及要在啟動(dòng)時(shí)傳遞給線程的任何數(shù)據(jù)。 以下示例顯示了使用當(dāng)前對(duì)象的自定義方法生成線程的此方法的基本調(diào)用。
[NSThread detachNewThreadSelector:@selector(myThreadMainMethod :) toTarget:self withObject:nil];
在OS X v10.5之前,您主要使用NSThread
類(lèi)來(lái)生成線程。 雖然可以獲得一個(gè)NSThread
對(duì)象并訪問(wèn)一些線程屬性,但是您只能在線程本身運(yùn)行后才能執(zhí)行此操作。 在OS X v10.5中,添加了創(chuàng)建NSThread
對(duì)象的支持,而不會(huì)立即產(chǎn)生相應(yīng)的新線程。 (此支持也可在iOS中使用。)此支持可以在啟動(dòng)線程之前獲取和設(shè)置各種線程屬性。 它也可以使用該線程對(duì)象稍后引用正在運(yùn)行的線程。
在OS X v10.5及更高版本中初始化NSThread
對(duì)象的簡(jiǎn)單方法是使用[initWithTarget:selector:object:]
method。 此方法與detachNewThreadSelector:toTarget:withObject:
method完全相同的信息,并使用它來(lái)初始化新的NSThread
實(shí)例。 但是,它不啟動(dòng)線程。 要啟動(dòng)線程,您將顯式調(diào)用線程對(duì)象的start方法,如以下示例所示:
NSThread * myThread = [[NSThread alloc] initWithTarget:self
selector:@selector(myThreadMainMethod :)
對(duì)象:nil];
[myThread start];
注意:使用initWithTarget:selector:object:方法的initWithTarget:selector:object:一種方法是將NSThread子類(lèi)化并覆蓋其main方法。 您將使用此方法的覆蓋版本來(lái)實(shí)現(xiàn)線程的主入口點(diǎn)。
如果您的線程當(dāng)前正在運(yùn)行的NSThread
對(duì)象,您可以向該線程發(fā)送消息的一種方法是使用應(yīng)用程序中幾乎任何對(duì)象的`[performSelector:onThread:withObject:waitUntilDone:]
方法。 在線程上執(zhí)行的選擇器支持(主線程除外)在OS X v10.5中引入,是一種方便的線程通信方式。 (此支持也可在iOS中使用。)使用此技術(shù)發(fā)送的消息由另一個(gè)線程直接執(zhí)行,作為其正常運(yùn)行循環(huán)處理的一部分。 (當(dāng)然,這意味著目標(biāo)線程必須在其運(yùn)行循環(huán)中運(yùn)行)當(dāng)您以這種方式進(jìn)行通信時(shí),您可能仍需要某種形式的同步,但它比在線程。
注意:盡管線程之間的偶爾通信很有用,但不應(yīng)該使用performSelector:onThread:withObject:waitUntilDone:
方法來(lái)對(duì)線程之間進(jìn)行時(shí)間關(guān)鍵或頻繁的通信。
使用NSObject生成線程
在iOS和OS X v10.5及更高版本中,所有對(duì)象都有能力產(chǎn)生一個(gè)新的線程并使用它來(lái)執(zhí)行其中的一個(gè)方法。[performSelectorInBackground:withObject:]
method創(chuàng)建一個(gè)新的脫機(jī)線程,并使用指定的方法作為新線程的入口點(diǎn)。 例如,如果您有一些對(duì)象(由變量myObj
),并且該對(duì)象具有要在后臺(tái)線程中運(yùn)行的名為doSomething
的方法,則可以使用以下代碼:
[myObj performSelectorInBackground:@selector(doSomething)withObject:nil];
調(diào)用此方法的效果與調(diào)用當(dāng)前對(duì)象,選擇器和參數(shù)對(duì)象作為參數(shù)的[NSThread]
的[detachNewThreadSelector:toTarget:withObject:]
方法一樣。 使用默認(rèn)配置立即生成新線程,并開(kāi)始運(yùn)行。 在選擇器中,您必須像線程一樣配置線程。 例如,您將需要設(shè)置一個(gè)自動(dòng)釋放池(如果您沒(méi)有使用垃圾回收),并且如果您計(jì)劃使用它,則配置該線程的運(yùn)行循環(huán)。
配置線程屬性
創(chuàng)建線程后,有時(shí),您可能需要配置線程環(huán)境的不同部分。 以下部分介紹您可以進(jìn)行的一些更改以及何時(shí)進(jìn)行更改。
配置線程的堆棧大小
對(duì)于您創(chuàng)建的每個(gè)新線程,系統(tǒng)將在進(jìn)程空間中分配一定量的內(nèi)存,以充當(dāng)該線程的堆棧。 堆棧管理堆棧幀,也是線程的任何局部變量被聲明的地方。 為線程分配的內(nèi)存量列在線程成本中。
如果要更改給定線程的堆棧大小,則必須先創(chuàng)建線程。 所有線程技術(shù)都提供了一些設(shè)置堆棧大小的方法,盡管使用NSThread
設(shè)置堆棧大小只能在iOS和OS X v10.5及更高版本中使用。 表2列出了各種技術(shù)的不同選項(xiàng)。
配置線程本地存儲(chǔ)
每個(gè)線程都維護(hù)一個(gè)鍵值對(duì)的字典,可以從線程中的任何位置訪問(wèn)。 您可以使用此字典來(lái)存儲(chǔ)在執(zhí)行線程期間要保留的信息。 例如,您可以使用它來(lái)存儲(chǔ)要通過(guò)線程的運(yùn)行循環(huán)的多次迭代來(lái)保持的狀態(tài)信息。
Cocoa和POSIX以不同的方式存儲(chǔ)線程字典,所以您不能混合和匹配兩種技術(shù)的調(diào)用。 只要您在線程代碼中使用一種技術(shù),最終結(jié)果應(yīng)該是相似的。 在Cocoa中,您可以使用NSThread
對(duì)象的threadDictionary
方法來(lái)檢索NSMutableDictionary對(duì)象,您可以向其中添加線程所需的任何鍵。 在POSIX中,您可以使用pthread_setspecific
和pthread_getspecific
函數(shù)來(lái)設(shè)置和獲取線程的鍵值。
設(shè)置線程的分離狀態(tài)
默認(rèn)情況下,大多數(shù)高級(jí)線程技術(shù)都會(huì)創(chuàng)建脫機(jī)線程。 在大多數(shù)情況下,分離的線程是首選的,因?yàn)樗鼈冊(cè)试S系統(tǒng)在線程完成后立即釋放線程的數(shù)據(jù)結(jié)構(gòu)。 分離的線程也不需要與程序的明確交互。 從線程檢索結(jié)果的方法由您自行決定。 相比之下,系統(tǒng)不會(huì)回收可連接線程的資源,直到另一個(gè)線程與該線程明確連接,這個(gè)進(jìn)程可能會(huì)阻止執(zhí)行連接的線程。
您可以將可連接線程視為類(lèi)似于子線程。 雖然它們?nèi)匀蛔鳛楠?dú)立線程運(yùn)行,但是可以由另一個(gè)線程加入可連接線程,然后才能由系統(tǒng)回收其資源。 可連接線程還提供了將數(shù)據(jù)從退出線程傳遞到另一個(gè)線程的顯式方法。 在它退出之前,可連接線程可以將數(shù)據(jù)指針或其他返回值傳遞給pthread_exit函數(shù)。 另一個(gè)線程可以通過(guò)調(diào)用pthread_join函數(shù)來(lái)聲明此數(shù)據(jù)。
重要提示:在應(yīng)用程序退出時(shí),分離的線程可以立即終止,但可連接的線程不能。 在允許進(jìn)程退出之前,必須連接每個(gè)可連接的線程。 因此,線程執(zhí)行不應(yīng)中斷的關(guān)鍵工作(例如將數(shù)據(jù)保存到磁盤(pán))時(shí),可以使用可加入的線程。
如果你想創(chuàng)建可連接的線程,唯一的方法是使用POSIX線程。 默認(rèn)情況下,POSIX將線程創(chuàng)建為可連接。 要將線程標(biāo)記為分離或可連接,請(qǐng)?jiān)趧?chuàng)建線程之前使用pthread_attr_setdetachstate函數(shù)修改線程屬性。 線程開(kāi)始后,您可以通過(guò)調(diào)用pthread_detach函數(shù)將可連接線程更改為分離的線程。 有關(guān)這些POSIX線程函數(shù)的更多信息,請(qǐng)參閱 pthread 手冊(cè)頁(yè)。
設(shè)置線程的優(yōu)先級(jí)
您創(chuàng)建的任何新線程都具有與之關(guān)聯(lián)的默認(rèn)優(yōu)先級(jí)。 內(nèi)核的調(diào)度算法在確定要運(yùn)行的線程時(shí)考慮線程優(yōu)先級(jí),優(yōu)先級(jí)更高的線程比具有較低優(yōu)先級(jí)的線程更可能運(yùn)行。 較高的優(yōu)先級(jí)不能保證線程的特定執(zhí)行時(shí)間,只要與較低優(yōu)先級(jí)的線程進(jìn)行比較時(shí),調(diào)度程序更有可能選擇它。
重要提示:將線程的優(yōu)先級(jí)保留為默認(rèn)值是一個(gè)好主意。 增加某些線程的優(yōu)先級(jí)也會(huì)增加低優(yōu)先級(jí)線程之間的饑餓的可能性。 如果您的應(yīng)用程序包含必須互相交互的高優(yōu)先級(jí)和低優(yōu)先級(jí)的線程,則低優(yōu)先級(jí)線程的饑餓可能會(huì)阻止其他線程并創(chuàng)建性能瓶頸。
如果你想修改線程優(yōu)先級(jí),Cocoa和POSIX都提供了一種方法。 對(duì)于Cocoa線程,可以使用NSThread
的` [setThreadPriority:]
class方法設(shè)置當(dāng)前正在運(yùn)行的線程的優(yōu)先級(jí)。 對(duì)于POSIX線程,可以使用pthread_setschedparam
函數(shù)。
寫(xiě)你的線程進(jìn)入進(jìn)程
在大多數(shù)情況下,線程的入口點(diǎn)進(jìn)程的結(jié)構(gòu)與其他平臺(tái)上的OS X相同。 您初始化您的數(shù)據(jù)結(jié)構(gòu),做一些工作或可選地設(shè)置一個(gè)運(yùn)行循環(huán),并在線程的代碼完成時(shí)清理。 根據(jù)您的設(shè)計(jì),編寫(xiě)入門(mén)程序時(shí)可能需要執(zhí)行一些額外的步驟。
創(chuàng)建自動(dòng)釋放池
在Objective-C框架中鏈接的應(yīng)用程序通常必須在其每個(gè)線程中至少創(chuàng)建一個(gè)自動(dòng)釋放池。 如果應(yīng)用程序使用托管模型(應(yīng)用程序處理對(duì)象的保留和釋放),則自動(dòng)釋放池捕獲從該線程自動(dòng)釋放的任何對(duì)象。
如果應(yīng)用程序使用垃圾收集而不是托管內(nèi)存模型,則不需要?jiǎng)?chuàng)建自動(dòng)釋放池。 垃圾收集應(yīng)用程序中的自動(dòng)釋放池的存在并不是有害的,大多數(shù)情況下都被忽略。 允許代碼模塊必須同時(shí)支持垃圾收集和托管內(nèi)存模式的情況。 在這種情況下,必須存在自動(dòng)釋放池以支持托管內(nèi)存模型代碼,如果應(yīng)用程序運(yùn)行時(shí)啟用了垃圾收集,則會(huì)被忽略。
如果您的應(yīng)用程序使用托管內(nèi)存模型,則創(chuàng)建自動(dòng)釋放池應(yīng)該是您在線程進(jìn)入例程中執(zhí)行的第一件事。 同樣的,摧毀這個(gè)自動(dòng)釋放池應(yīng)該是您在線程中執(zhí)行的最后一件事。 該池確保自動(dòng)釋放的對(duì)象被捕獲,盡管它在線程本身退出之前不會(huì)釋放它們。
- (void)myThreadMainRoutine
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; //頂級(jí)池
//線程在線工作
[游泳池釋放]; //釋放池中的對(duì)象。
}
因?yàn)轫敿?jí)自動(dòng)釋放池在線程退出之前不會(huì)釋放其對(duì)象,因此長(zhǎng)時(shí)間的線程應(yīng)該創(chuàng)建更多的自動(dòng)釋放池來(lái)更頻繁地釋放對(duì)象。 例如,使用運(yùn)行循環(huán)的線程可能會(huì)每次通過(guò)該運(yùn)行循環(huán)創(chuàng)建和釋放自動(dòng)釋放池。 更頻繁地釋放對(duì)象可以防止應(yīng)用程序的內(nèi)存占用不斷增長(zhǎng),從而導(dǎo)致性能問(wèn)題。 與任何性能相關(guān)的行為一樣,您應(yīng)該衡量代碼的實(shí)際性能,并適當(dāng)調(diào)整您對(duì)自動(dòng)釋放池的使用。
設(shè)置異常處理程序
如果您的應(yīng)用程序捕獲并處理異常,則您的線程代碼應(yīng)準(zhǔn)備好捕獲可能發(fā)生的任何異常。 盡管最好在可能發(fā)生的情況下處理異常,但如果線程中拋出異常,則無(wú)法使您的應(yīng)用程序退出。 在你的線程入口例程中安裝最終的try / catch可以讓你捕獲任何未知的異常并提供適當(dāng)?shù)捻憫?yīng)。
在Xcode中構(gòu)建項(xiàng)目時(shí),可以使用C ++或Objective-C異常處理風(fēng)格。
設(shè)置運(yùn)行循環(huán)
編寫(xiě)代碼時(shí)要在單獨(dú)的線程上運(yùn)行,您有兩個(gè)選項(xiàng)。 第一個(gè)選擇是將一個(gè)線程的代碼寫(xiě)為一個(gè)很長(zhǎng)的任務(wù)要執(zhí)行的很少或沒(méi)有中斷,并在完成線程退出。 第二個(gè)選項(xiàng)是讓你的線程進(jìn)入一個(gè)循環(huán),并讓它們?cè)诘竭_(dá)時(shí)動(dòng)態(tài)處理請(qǐng)求。 第一個(gè)選項(xiàng)不需要您的代碼的特殊設(shè)置; 你只是開(kāi)始做你想做的工作。 但是,第二個(gè)選項(xiàng)涉及設(shè)置線程的運(yùn)行循環(huán)。
OS X和iOS提供內(nèi)置的支持,可在每個(gè)線程中實(shí)現(xiàn)運(yùn)行循環(huán)。 應(yīng)用程序框架自動(dòng)啟動(dòng)應(yīng)用程序主線程的運(yùn)行循環(huán)。 如果創(chuàng)建任何輔助線程,則必須配置運(yùn)行循環(huán)并手動(dòng)啟動(dòng)它。
終止線程
退出線程的推薦方法是讓它正常退出其入口點(diǎn)例程。 雖然Cocoa,POSIX和多處理服務(wù)提供了直接殺死線程的例程,但是強(qiáng)烈建議不要使用此類(lèi)例程。 殺死線程會(huì)阻止線程自身清理。 線程分配的內(nèi)存可能會(huì)泄漏,線程當(dāng)前正在使用的任何其他資源可能無(wú)法正確清除,從而在以后創(chuàng)建潛在問(wèn)題。
如果您期望在操作中終止線程,您應(yīng)該從一開(kāi)始就設(shè)計(jì)線程以響應(yīng)取消或退出消息。 對(duì)于長(zhǎng)時(shí)間運(yùn)行的操作,這可能意味著定期停止工作,并檢查是否有消息到達(dá)。 如果一條消息確實(shí)要求線程退出,線程將有機(jī)會(huì)執(zhí)行任何所需的清理并正常退出; 否則,它可以簡(jiǎn)單地返回工作并處理下一個(gè)數(shù)據(jù)塊。
響應(yīng)取消消息的一種方法是使用運(yùn)行循環(huán)輸入源來(lái)接收這樣的消息。 代碼2-3顯示了該代碼在線程的主入口例程中的外觀結(jié)構(gòu)。 (該示例僅顯示主循環(huán)部分,不包括設(shè)置自動(dòng)釋放池或配置實(shí)際工作的步驟。)示例在運(yùn)行循環(huán)上安裝自定義輸入源,可能會(huì)從另一個(gè)你的線程 有關(guān)設(shè)置輸入源的信息,在執(zhí)行總工作量的一部分后,線程會(huì)短暫運(yùn)行運(yùn)行循環(huán),以查看消息是否到達(dá)輸入源。 如果沒(méi)有,運(yùn)行循環(huán)將立即退出,循環(huán)繼續(xù)下一個(gè)工作塊。 因?yàn)樘幚沓绦虿荒苤苯釉L問(wèn)exitNow
局部變量,所以退出條件通過(guò)線程字典中的鍵值對(duì)來(lái)傳達(dá)。
- (void)threadMainRoutine
{
BOOL moreWorkToDo = YES;
BOOL exitNow = NO;
NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
//將exitNow BOOL添加到線程字典。
NSMutableDictionary * threadDict = [[NSThread currentThread] threadDictionary];
[threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@“ThreadShouldExitNow”];
//安裝輸入源。
[self myInstallCustomInputSource];
while(moreWorkToDo &&!exitNow)
{
//在這里做一大塊工作。
//完成后更改moreWorkToDo Boolean的值。
//運(yùn)行運(yùn)行循環(huán),如果輸入源不等待觸發(fā)則立即超時(shí)。
[runLoop runUntilDate:[NSDate date]];
//檢查輸入源處理程序是否更改了exitNow值。
exitNow = [[threadDict valueForKey:@“ThreadShouldExitNow”] boolValue];
}
}