線程管理Mac OS X和iOS里面的每個進程都是有一個或多個線程構成,每個線程都代表一個代碼的執(zhí)行路徑。每個應用程序啟動時候都是一個線程,它執(zhí)行程序的main函數(shù)。應用程序可以生成額外的線程,其中每個線程執(zhí)行一個特定功能的代碼。 當應用程序生成一個新的線程的時候,該線程變成應用程序進程空間內的一個實體。每個線程都擁有它自己的執(zhí)行堆棧,由內核調度獨立的運行時間片。一個線程可以和其他線程或其他進程通信,執(zhí)行I/O操作,甚至執(zhí)行任何你想要它完成的任務。因為它們處于相同的進程空間,所以一個獨立應用程序里面的所有線程共享相同的虛擬內存空間,并且具有和進程相同的訪問權限。 本章提供了Mac OS X和iOS上面可用線程技術的預覽,并給出了如何在你的應用程序里面使用它們的例子。
1.1 線程成本多線程會占用你應用程序(和系統(tǒng)的)的內存使用和性能方面的資源。每個線程都需要分配一定的內核內存和應用程序內存空間的內存。管理你的線程和協(xié)調其調度所需的核心數(shù)據結構存儲在使用Wired Memory的內核里面。你線程的堆??臻g和每個線程的數(shù)據都被存儲在你應用程序的內存空間里面。這些數(shù)據結構里面的大部分都是當你首次創(chuàng)建線程或者進程的時候被創(chuàng)建和初始化的,它們所需的代價成本很高,因為需要和內核交互。 表2-1量化了在你應用程序創(chuàng)建一個新的用戶級線程所需的大致成本。這些成本里面的部分是可配置的,比如為輔助線程分配堆??臻g的大小。創(chuàng)建一個線程所需的時間成本是粗略估計的,僅用于當互相比較的時候。線程創(chuàng)建時間很大程度依賴于處理器的負載,計算速度,和可用的系統(tǒng)和程序空間。 Table 2-1 Thread creation costs
注意:因為底層內核的支持,操作對象(Operation objectis)可能創(chuàng)建線程更快。它們使用內核里面常駐線程池里面的線程來節(jié)省創(chuàng)建的時間,而不是每次都創(chuàng)建新的線程。關于更多使用操作對象(Operation objects)的信息,參閱并發(fā)編程指南(Concurrency Programming Guide)。 當編寫線程代碼時另外一個需要考慮的成本是生產成本。設計一個線程應用程序有時會需要根本性改變你應用程序數(shù)據結構的組織方式。要做這些改變可能需要避免使用同步,因為本身設計不好的應用可能會造成巨大的性能損失。設計這些數(shù)據結構和在線程代碼里面調試問題會增加開發(fā)一個線程應用所需的時間。然而避免這些消耗的話,可能在運行時候帶來更大的問題,如果你的多線程花費太多的時間在鎖的等待而沒有做任何事情。 1.1 創(chuàng)建一個線程創(chuàng)建低級別的線程相對簡單。在所有情況下,你必須有一個函數(shù)或方法作為線程的主入口點,你必須使用一個可用的線程例程啟動你的線程。以下幾個部分介紹了比較常用線程創(chuàng)建的基本線程技術。線程創(chuàng)建使用了這些技術的繼承屬性的默認設置,由你所使用的技術來決定。關于更多如何配置你的線程的信息,參閱“線程屬性配置”部分。 1.1.1 使用NSThread使用NSThread來創(chuàng)建線程有兩個可以的方法:
這兩種創(chuàng)建線程的技術都在你的應用程序里面新建了一個脫離的線程。一個脫離的線程意味著當線程退出的時候線程的資源由系統(tǒng)自動回收。這也同樣意味著之后不需要在其他線程里面顯式的連接(join)。因為detachNewThreadSelctor:toTarget:withObject:方法在Mac OS X的任何版本都支持,所以在Cocoa應用里面使用多線程的地方經??梢园l(fā)現(xiàn)它。為了生成一個新的線程,你只要簡單的提供你想要使用為線程主體入口的方法的名稱(被指定為一個selector),和任何你想在啟動時傳遞給線程的數(shù)據。下面的示例演示了這種方法的基本調用,來使用當前對象的自定義方法來生成一個線程。
在Mac OS X v10.5及其之后初始化一個NSThread對象的簡單方法是使用initWithTarget:selector:object:方法。該方法和detachNewThreadSelector:toTarget:withObject:方法來初始化一個新的NSThread實例需要相同的額外開銷。然而它并沒有啟動一個線程。為了啟動一個線程,你可以顯式調用先對象的start方法,如下面代碼: 注意:使用initWithTarget:selector:object:方法的替代辦法是子類化NSThread,并重寫它的main方法。你可以使用你重寫的該方法的版本來實現(xiàn)你線程的主體入口。更多信息,請參閱NSThread Class Reference里面子類化的提示。 如果你擁有一個NSThread對象,它的線程當前真正運行,你可以給該線程發(fā)送消息的唯一方法是在你應用程序里面的任何對象使用performSelector:onThread:withObject:waitUntilDone:方法。在Mac OS X v10.5支持在多線程上面執(zhí)行selectors(而不是在主線程里面),并且它是實現(xiàn)線程間通信的便捷方法。你使用該技術時所發(fā)送的消息會被其他線程作為run-loop主體的一部分直接執(zhí)行(當然這些意味著目標線程必須在它的run loop里面運行,參閱“ Run Loops”)。當你使用該方法來實現(xiàn)線程通信的時候,你可能仍然需要一個同步操作,但是這比在線程間設置通信端口簡單多了。 注意:雖然在線程間的偶爾通信的時候使用該方法很好,但是你不能周期的或頻繁的使用performSelector:onThread:withObject:waitUntilDone:來實現(xiàn)線程間的通信。 關于線程間通信的可選方法,參閱“設置線程的脫離狀態(tài)”部分。 1.1.2 使用POSIX的多線程Mac OS X和iOS提供基于C語言支持的使用POSIX線程API來創(chuàng)建線程的方法。該技術實際上可以被任何類型的應用程序使用(包括Cocoa和Cocoa Touch的應用程序),并且如果你當前真為多平臺開發(fā)應用的話,該技術可能更加方便。你使用來創(chuàng)建線程的POSIX例程被調用的時候,使用pthread_create剛好足夠。 列表2-1顯示了兩個使用POSIX來創(chuàng)建線程的自定義函數(shù)。LaunchThread函數(shù)創(chuàng)建了一個新的線程,該線程的例程由PosixThreadMainRoutine函數(shù)來實現(xiàn)。因為POSIX創(chuàng)建的線程默認情況是可連接的(joinable),下面的例子改變線程的屬性來創(chuàng)建一個脫離的線程。把線程標記為脫離的,當它退出的時候讓系統(tǒng)有機會立即回收該線程的資源。 Listing 2-1 Creating a thread in C
如果你把上面列表的代碼添加到你任何一個源文件,并且調用LaunchThread函數(shù),它將會在你的應用程序里面創(chuàng)建一個新的脫離線程。當然,新創(chuàng)建的線程使用該代碼沒有做任何有用的事情。線程將會加載并立即退出。為了讓它更有興趣,你需要添加代碼到PosixThreadMainRoutine函數(shù)里面來做一些實際的工作。為了保證線程知道該干什么,你可以在創(chuàng)建的時候給線程傳遞一個數(shù)據的指針。把該指針作為pthread_create的最后一個參數(shù)。 為了在新建的線程里面和你應用程序的主線程通信,你需要建立一條和目標線程之間的穩(wěn)定的通信路徑。對于基于C語言的應用程序,有幾種辦法來實現(xiàn)線程間的通信,包括使用端口(ports),條件(conditions)和共享內存(shared memory)。對于長期存在的線程,你應該幾乎總是成立某種線程間的通信機制,讓你的應用程序的主線程有辦法來檢查線程的狀態(tài)或在應用程序退出時干凈關閉它。 關于更多介紹POSIX線程函數(shù)的信息,參閱pthread的主頁。 1.1.3 使用NSObject來生成一個線程在iOS和Mac OS X v10.5及其之后,所有的對象都可能生成一個新的線程,并用它來執(zhí)行它任意的方法。方法performSelectorInBackground:withObject:新生成一個脫離的線程,使用指定的方法作為新線程的主體入口點。比如,如果你有一些對象(使用變量myObj來代表),并且這些對象擁有一個你想在后臺運行的doSomething的方法,你可以使用如下的代碼來生成一個新的線程: 調用該方法的效果和你在當前對象里面使用NSThread的detachNewThreadSelector:toTarget:withObject:傳遞selectore,object作為參數(shù)的方法一樣。新的線程將會被立即生成并運行,它使用默認的設置。在selectore內部,你必須配置線程就像你在任何線程里面一樣。比如,你可能需要設置一個自動釋放池(如果你沒有使用垃圾回收機制),在你要使用它的時候配置線程的run loop。關于更是介紹如果配置線程的信息,參閱“配置線程屬性”部分。 1.1.4 使用其他線程技術盡管POSIX例程和NSThread類被推薦使用來創(chuàng)建低級線程,但是其他基于C語言的技術在Mac OS X上面同樣可用。在這其中,唯一一個可以考慮使用的是多處理服務(Multiprocessing Services),它本身就是在POSIX線程上執(zhí)行。多處理服務是專門為早期的Mac OS版本開發(fā)的,后來在Mac OS X里面的Carbon應用程序上面同樣適用。如果你有代碼真是有該技術,你可以繼續(xù)使用它,盡管你應該把這些代碼轉化為POSIX。該技術在iOS上面不可用。 關于更多如何使用多處理服務的信息,參閱多處理服務編程指南(Multiprocessing Services Programming Guide)。 1.1.5 在Cocoa程序上面使用POSIX線程經管NSThread類是Cocoa應用程序里面創(chuàng)建多線程的主要接口,如果可以更方便的話你可以任意使用POSIX線程帶替代。例如,如果你的代碼里面已經使用了它,而你又不想改寫它的話,這時你可能需要使用POSIX多線程。如果你真打算在Cocoa程序里面使用POSIX線程,你應該了解如果在Cocoa和線程間交互,并遵循以下部分的一些指南。 u Cocoa框架的保護 對于多線程的應用程序,Cocoa框架使用鎖和其他同步方式來保證代碼的正確執(zhí)行。為了保護這些鎖造成在單線程里面性能的損失,Cocoa直到應用程序使用NSThread類生成它的第一個新的線程的時候才創(chuàng)建這些鎖。如果你僅且使用POSIX例程來生成新的線程,Cocoa不會收到關于你的應用程序當前變?yōu)槎嗑€程的通知。當這些剛好發(fā)生的時候,涉及Cocoa框架的操作哦可能會破壞甚至讓你的應用程序崩潰。 為了讓Cocoa知道你正打算使用多線程,你所需要做的是使用NSThread類生成一個線程,并讓它立即退出。你線程的主體入口點不需要做任何事情。只需要使用NSThread來生成一個線程就足夠保證Cocoa框架所需的鎖到位。 如果你不確定Cocoa是否已經知道你的程序是多線程的,你可以使用NSThread的isMultiThreaded方法來檢驗一下。 u 混合POSIX和Cocoa的鎖 在同一個應用程序里面混合使用POSIX和Cocoa的鎖很安全。Cocoa鎖和條件對象基本上只是封裝了POSIX的互斥體和條件。然而給定一個鎖,你必須總是使用同樣的接口來創(chuàng)建和操縱該鎖。換言之,你不能使用Cocoa的NSLock對象來操縱一個你使用pthread_mutex_init函數(shù)生成的互斥體,反之亦然。 1.2 配置線程屬性創(chuàng)建線程之后,或者有時候是之前,你可能需要配置不同的線程環(huán)境。以下部分描述了一些你可以做的改變,和在什么時候你需要做這些改變。 1.2.1 配置線程的堆棧大小對于每個你新創(chuàng)建的線程,系統(tǒng)會在你的進程空間里面分配一定的內存作為該線程的堆棧。該堆棧管理堆棧幀,也是任何線程局部變量聲明的地方。給線程分配的內存大小在“線程成本”里面已經列舉了。 如果你想要改變一個給定線程的堆棧大小,你必須在創(chuàng)建該線程之前做一些操作。所有的線程技術提供了一些辦法來設置線程堆棧的大小。雖然可以使用NSThread來設置堆棧大小,但是它只能在iOS和Mac OS X v10.5及其之后才可用。表2-2列出了每種技術的對于不同的操作。 Table 2-2 Setting the stack size of a thread
1.2.2 配置線程本地存儲每個線程都維護了一個鍵-值的字典,它可以在線程里面的任何地方被訪問。你可以使用該字典來保存一些信息,這些信息在整個線程的執(zhí)行過程中都保持不變。比如,你可以使用它來存儲在你的整個線程過程中Run loop里面多次迭代的狀態(tài)信息。 Cocoa和POSIX以不同的方式保存線程的字典,所以你不能混淆并同時調用者兩種技術。然而只要你在你的線程代碼里面堅持使用了其中一種技術,最終的結果應該是一樣的。在Cocoa里面,你使用NSThread的threadDictionary方法來檢索一個NSMutableDictionary對象,你可以在它里面添加任何線程需要的鍵。在POSIX里面,你使用pthread_setspecific和pthread_getspecific函數(shù)來設置和訪問你線程的鍵和值。 1.2.3 設置線程的脫離狀態(tài)大部分上層的線程技術都默認創(chuàng)建了脫離線程(Datached thread)。大部分情況下,脫離線程(Detached thread)更受歡迎,因為它們允許系統(tǒng)在線程完成的時候立即釋放它的數(shù)據結構。脫離線程同時不需要顯示的和你的應用程序交互。意味著線程檢索的結果由你來決定。相比之下,系統(tǒng)不回收可連接線程(Joinable thread)的資源直到另一個線程明確加入該線程,這個過程可能會阻止線程執(zhí)行加入。 你可以認為可連接線程類似于子線程。雖然你作為獨立線程運行,但是可連接線程在它資源可以被系統(tǒng)回收之前必須被其他線程連接。可連接線程同時提供了一個顯示的方式來把數(shù)據從一個正在退出的線程傳遞到其他線程。在它退出之前,可連接線程可以傳遞一個數(shù)據指針或者其他返回值給pthread_exit函數(shù)。其他線程可以通過pthread_join函數(shù)來拿到這些數(shù)據。 重要:在應用程序退出時,脫離線程可以立即被中斷,而可連接線程則不可以。每個可連接線程必須在進程被允許可以退出的時候被連接。所以當線程處于周期性工作而不允許被中斷的時候,比如保存數(shù)據到硬盤,可連接線程是最佳選擇。 如果你想要創(chuàng)建可連接線程,唯一的辦法是使用POSIX線程。POSIX默認創(chuàng)建的線程是可連接的。為了把線程標記為脫離的或可連接的,使用pthread_attr_setdetachstate函數(shù)來修改正在創(chuàng)建的線程的屬性。在線程啟動后,你可以通過調用pthread_detach函數(shù)來把線程修改為可連接的。關于更多POSIX線程函數(shù)信息,參與pthread主頁。關于更多如果連接一個線程,參閱pthread_join的主頁。 1.2.4 設置線程的優(yōu)先級你創(chuàng)建的任何線程默認的優(yōu)先級是和你本身線程相同。內核調度算法在決定該運行那個線程時,把線程的優(yōu)先級作為考量因素,較高優(yōu)先級的線程會比較低優(yōu)先級的線程具有更多的運行機會。較高優(yōu)先級不保證你的線程具體執(zhí)行的時間,只是相比較低優(yōu)先級的線程,它更有可能被調度器選擇執(zhí)行而已。 重要:讓你的線程處于默認優(yōu)先級值是一個不錯的選擇。增加某些線程的優(yōu)先級,同時有可能增加了某些較低優(yōu)先級線程的饑餓程度。如果你的應用程序包含較高優(yōu)先級和較低優(yōu)先級線程,而且它們之間必須交互,那么較低優(yōu)先級的饑餓狀態(tài)有可能阻塞其他線程,并造成性能瓶頸。 如果你想改變線程的優(yōu)先級,Cocoa和POSIX都提供了一種方法來實現(xiàn)。對于Cocoa線程而言,你可以使用NSThread的setThreadPriority:類方法來設置當前運行線程的優(yōu)先級。對于POSIX線程,你可以使用pthread_setschedparam函數(shù)來實現(xiàn)。關于更多信息,參與NSThread Class Reference或pthread_setschedparam主頁。 1.3 編寫你線程的主體入口點對于大部分而言,Mac OS X上面線程結構的主體入口點和其他平臺基本一樣。你需要初始化你的數(shù)據結構,做一些工作或可行的設置一個run loop,并在線程代碼被執(zhí)行完后清理它。根據設計,當你寫的主體入口點的時候有可能需要采取一些額外的步驟。 1.3.1 創(chuàng)建一個自動釋放池(Autorelease Pool)在Objective - C框架鏈接的應用程序,通常在它們的每一個線程必須創(chuàng)建至少一個自動釋放池。如果應用程序使用管理模型,即應用程序處理的retain和release對象,那么自動釋放池捕獲任何從該線程autorelease的對象。 如果應用程序使用的垃圾回收機制,而不是管理的內存模型,那么創(chuàng)建一個自動釋放池不是絕對必要的。在垃圾回收的應用程序里面,一個自動釋放池是無害的,而且大部分情況是被忽略。允許通過個代碼管理必須同時支持垃圾回收和內存管理模型。在這種情況下,內存管理模型必須支持自動釋放池,當應用程序運行垃圾回收的時候,自動釋放池只是被忽略而已。 如果你的應用程序使用內存管理模型,在你編寫線程主體入口的時候第一件事情就是創(chuàng)建一個自動釋放池。同樣,在你的線程最后應該銷毀該自動釋放池。該池保證自動釋放。雖然對象被調用,但是它們不被release直到線程退出。列表2-2顯示了線程主體入口使用自動釋放池的基本結構。 Listing 2-2 Defining your thread entry point routine 因為高級的自動釋放池不會釋放它的對象直到線程退出。長時運行的線程需求新建額外的自動釋放池來更頻繁的釋放它的對象。比如,一個使用run loop的線程可能在每次運行完一次循環(huán)的時候創(chuàng)建并釋放該自動釋放池。更頻繁的釋放對象可以防止你的應用程序內存占用太大造成性能問題。雖然對于任何與性能相關的行為,你應該測量你代碼的實際表現(xiàn),并適當?shù)卣{整使用自動釋放池。 關于更多內存管理的信息和自動釋放池,參閱“內存高級管理編程指南(Advanced Memory Management Programming Guide)”。 1.3.2 設置異常處理如果你的應用程序捕獲并處理異常,那么你的線程代碼應該時刻準備捕獲任何可能發(fā)生的異常。雖然最好的辦法是在異常發(fā)生的地方捕獲并處理它,但是如果在你的線程里面捕獲一個拋出的異常失敗的話有可能造成你的應用程序強退。在你線程的主體入口點安裝一個try/catch模塊,可以讓你捕獲任何未知的異常,并提供一個合適的響應。 當在Xcode構建你項目的時候,你可以使用C 或者Objective-C的異常處理風格。 關于更多設置如何在Objective-C里面拋出和捕獲異常的信息,參閱Exception Programming Topics。 1.3.3 設置一個Run Loop當你想編寫一個獨立運行的線程時,你有兩種選擇。第一種選擇是寫代碼作為一個長期的任務,很少甚至不中斷,線程完成的時候退出。第二種選擇是把你的線程放入一個循環(huán)里面,讓它動態(tài)的處理到來的任務請求。第一種方法不需要在你的代碼指定任何東西;你只需要啟動的時候做你打算做的事情即可。然而第二種選擇需要在你的線程里面添加一個run loop。 Mac OS X和iOS提供了在每個線程實現(xiàn)run loop內置支持。Cocoa、Carbon和UIKit自動在你應用程序的主線程啟動一個run loop,但是如果你創(chuàng)建任何輔助線程,你必須手工的設置一個run loop并啟動它。 關于更多使用和配置run loop的信息,參閱“Run Loops”部分。 1.4 中斷線程退出一個線程推薦的方法是讓它在它主體入口點正常退出。經管Cocoa、POSIX和Multiprocessing Services提供了直接殺死線程的例程,但是使用這些例程是強烈不鼓勵的。殺死一個線程阻止了線程本身的清理工作。線程分配的內存可能造成泄露,并且其他線程當前使用的資源可能沒有被正確清理干凈,之后造成潛在的問題。 如果你的應用程序需要在一個操作中間中斷一個線程,你應該設計你的線程響應取消或退出的消息。對于長時運行的操作,這意味著周期性停止工作來檢查該消息是否到來。如果該消息的確到來并要求線程退出,那么線程就有機會來執(zhí)行任何清理和退出工作;否則,它返回繼續(xù)工作和處理下一個數(shù)據塊。 響應取消消息的一個方法是使用run loop的輸入源來接收這些消息。列表2-3顯示了該結構的類似代碼在你的線程的主體入口里面是怎么樣的(該示例顯示了主循環(huán)部分,不包括設立一個自動釋放池或配置實際的工作步驟)。該示例在run loop上面安裝了一個自定義的輸入源,它可以從其他線程接收消息。關于更多設置輸入源的信息,參閱“配置Run Loop源”。執(zhí)行工作的總和的一部分后,線程運行的run loop來查看是否有消息抵達輸入源。如果沒有,run loop立即退出,并且循環(huán)繼續(xù)處理下一個數(shù)據塊。因為該處理器并沒有直接的訪問exitNow局部變量,退出條件是通過線程的字典來傳輸?shù)摹?/p> Listing 2-3 Checking for an exit condition during a long job
|
|