當應用程式產生一個新的線程的時候,該線程變成應用程式進程空間內的一個實體。每個線程都擁有它自己的執行堆棧,由核心調度獨立的已耗用時間片。一個線程可以和其他線程或其他進程通訊,執行I/O操作,甚至執行任何你想要它完成的任務。因為它們處於相同的進程空間,所以一個獨立應用程式裡面的所有線程共用相同的虛擬記憶體空間,並且具有和進程相同的存取權限。
一、線程成本
多線程會佔用你應用程式(和系統的)的記憶體使用量和效能方面的資源。每個線程都需要分配一定的核心記憶體和應用程式記憶體空間的記憶體。管理你的線程和協調其調度所需的核心資料結構儲存在使用Wired Memory的核心裡面。你線程的堆棧空間和每個線程的資料都被儲存在你應用程式的記憶體空間裡面。這些資料結構裡面的大部分都是當你首次建立線程或者進程的時候被建立和初始化的,它們所需的代價成本很高,因為需要和核心互動。
二、 建立一個線程
1、使用NSThread
使用NSThread來建立線程有兩個可以的方法:
2、使用POSIX的多線程
Mac OS X和iOS提供基於C語言支援的使用POSIX線程API來建立線程的方法。該技術實際上可以被任何類型的應用程式使用(包括Cocoa和Cocoa Touch的應用程式),並且如果你當前真為多平台開發應用的話,該技術可能更加方便。
下面顯示了兩個使用POSIX來建立線程的自訂函數。LaunchThread函數建立了一個新的線程,該線程的常式由PosixThreadMainRoutine函數來實現。因為POSIX建立的線程預設情況是可串連的(joinable),下面的例子改變線程的屬性來建立一個脫離的線程。把線程標記為脫離的,當它退出的時候讓系統有機會立即回收該線程的資源。
#include <assert.h><pthread.h>* PosixThreadMainRoutine(* = pthread_attr_init(&!= pthread_attr_setdetachstate(&! threadError = pthread_create(&posixThreadID, &attr, &= pthread_attr_destroy(&! (threadError !=
如果你把上面列表的代碼添加到你任何一個源檔案,並且調用LaunchThread函數,它將會在你的應用程式裡面建立一個新的脫離線程。當然,新建立的線程使用該代碼沒有做任何有用的事情。線程將會載入並立即退出。為了讓它更有興趣,你需要添加代碼到PosixThreadMainRoutine函數裡面來做一些實際的工作。為了保證線程知道該幹什麼,你可以在建立的時候給線程傳遞一個資料的指標。把該指標作為pthread_create的最後一個參數。
為了在建立的線程裡面和你應用程式的主線程通訊,你需要建立一條和目標線程之間的穩定的通訊路徑。對於基於C語言的應用程式,有幾種辦法來實現線程間的通訊,包括使用連接埠(ports),條件(conditions)和共用記憶體(shared memory)。對於長期存在的線程,你應該幾乎總是成立某種線程間的通訊機制,讓你的應用程式的主線程有辦法來檢查線程的狀態或在應用程式退出時乾淨關閉它。
關於更多介紹POSIX線程函數的資訊,參閱pthread的首頁。
3、使用NSObject來產生一個線程
在iOS和Mac OS X v10.5及其之後,所有的對象都可能產生一個新的線程,並用它來執行它任意的方法。方法performSelectorInBackground:withObject:新產生一個脫離的線程,使用指定的方法作為新線程的主體進入點。比如,如果你有一些對象(使用變數myObj來代表),並且這些對象擁有一個你想在後台啟動並執行doSomething的方法,你可以使用如下的代碼來產生一個新的線程:
[myObj performSelectorInBackground:@selector(doSomething) withObject:nil];
調用該方法的效果和你在當前對象裡面使用NSThread的detachNewThreadSelector:toTarget:withObject:傳遞selectore,object作為參數的方法一樣。新的線程將會被立即產生並運行,它使用預設的設定。在selectore內部,你必須配置線程就像你在任何線程裡面一樣。比如,你可能需要設定一個自動釋放池(如果你沒有使用記憶體回收機制),在你要使用它的時候配置線程的run loop。
4、 使用其他線程技術
儘管POSIX常式和NSThread類被推薦使用來建立低級線程,但是其他基於C語言的技術在Mac OS X上面同樣可用。在這其中,唯一一個可以考慮使用的是多處理服務(Multiprocessing Services),它本身就是在POSIX線程上執行。多處理服務是專門為早期的Mac OS版本開發的,後來在Mac OS X裡面的Carbon應用程式上面同樣適用。如果你有代碼真是有該技術,你可以繼續使用它,儘管你應該把這些代碼轉化為POSIX。該技術在iOS上面不可用。
關於更多如何使用多處理服務的資訊,參閱多處理服務編程指南(Multiprocessing Services Programming Guide)。
5、在Cocoa程式上面使用POSIX線程
經管NSThread類是Cocoa應用程式裡面建立多線程的主要介面,如果可以更方便的話你可以任意使用POSIX線程帶替代。例如,如果你的代碼裡面已經使用了它,而你又不想改寫它的話,這時你可能需要使用POSIX多線程。如果你真打算在Cocoa程式裡面使用POSIX線程,你應該瞭解如果在Cocoa和線程間互動,並遵循以下部分的一些指南。
u Cocoa架構的保護
對於多線程的應用程式,Cocoa架構使用鎖和其他同步方式來保證代碼的正確執行。為了保護這些鎖造成在單線程裡面效能的損失,Cocoa直到應用程式使用NSThread類產生它的第一個新的線程的時候才建立這些鎖。如果你僅且使用POSIX常式來產生新的線程,Cocoa不會收到關於你的應用程式當前變為多線程的通知。當這些剛好發生的時候,涉及Cocoa架構的操作哦可能會破壞甚至讓你的應用程式崩潰。
為了讓Cocoa知道你正打算使用多線程,你所需要做的是使用NSThread類產生一個線程,並讓它立即退出。你線程的主體進入點不需要做任何事情。只需要使用NSThread來產生一個線程就足夠保證Cocoa架構所需的鎖到位。
如果你不確定Cocoa是否已經知道你的程式是多線程的,你可以使用NSThread的isMultiThreaded方法來檢驗一下。
u 混合POSIX和Cocoa的鎖
在同一個應用程式裡面混合使用POSIX和Cocoa的鎖很安全。Cocoa鎖和條件對象基本上只是封裝了POSIX的互斥體和條件。然而給定一個鎖,你必須總是使用同樣的介面來建立和操縱該鎖。換言之,你不能使用Cocoa的NSLock對象來操縱一個你使用pthread_mutex_init函數產生的互斥體,反之亦然。
三、配置線程屬性
1、配置線程的堆棧大小
對於每個你新建立的線程,系統會在你的進程空間裡面分配一定的記憶體作為該線程的堆棧。該堆棧管理堆疊框架,也是任何線程局部變數聲明的地方。如果你想要改變一個給定線程的堆棧大小,你必須在建立該線程之前做一些操作。所有的線程技術提供了一些辦法來設定線程堆棧的大小。雖然可以使用NSThread來設定堆棧大小,但是它只能在iOS和Mac OS X v10.5及其之後才可用。表2-2列出了每種技術的對於不同的操作。
Technology |
Option |
Cocoa |
In iOS and Mac OS X v10.5 and later, allocate and initialize an NSThread object (do not use thedetachNewThreadSelector:toTarget:withObject: method). Before calling the start method of the thread object, use thesetStackSize: method to specify the new stack size. |
POSIX |
Create a new pthread_attr_t structure and use the pthread_attr_setstacksize function to change the default stack size. Pass the attributes to the pthread_create function when creating your thread. |
Multiprocessing Services |
Pass the appropriate stack size value to the MPCreateTask function when you create your thread. |
2、 配置執行緒區域儲存
每個線程都維護了一個鍵-值的字典,它可以線上程裡面的任何地方被訪問。你可以使用該字典來儲存一些資訊,這些資訊在整個線程的執行過程中都保持不變。比如,你可以使用它來儲存在你的整個線程過程中Run loop裡面多次迭代的狀態資訊。
Cocoa和POSIX以不同的方式儲存線程的字典,所以你不能混淆並同時調用者兩種技術。然而只要你在你的線程代碼裡面堅持使用了其中一種技術,最終的結果應該是一樣的。在Cocoa裡面,你使用NSThread的threadDictionary方法來檢索一個NSMutableDictionary對象,你可以在它裡面添加任何線程需要的鍵。在POSIX裡面,你使用pthread_setspecific和pthread_getspecific函數來設定和訪問你線程的鍵和值。
3、設定線程的脫離狀態
大部分上層的線程技術都預設建立了脫離線程(Datached thread)。大部分情況下,脫離線程(Detached thread)更受歡迎,因為它們允許系統線上程完成的時候立即釋放它的資料結構。脫離線程同時不需要顯示的和你的應用程式互動。意味著線程檢索的結果由你來決定。相比之下,系統不回收可連接線程(Joinable thread)的資源直到另一個線程明確加入該線程,這個過程可能會阻止線程執行加入。
你可以認為可連接線程類似於子線程。雖然你作為獨立線程運行,但是可連接線程在它資源可以被系統回收之前必須被其他線程串連。可連接線程同時提供了一個顯示的方式來把資料從一個正在退出的線程傳遞到其他線程。在它退出之前,可連接線程可以傳遞一個資料指標或者其他傳回值給pthread_exit函數。其他線程可以通過pthread_join函數來拿到這些資料。
重要:在應用程式退出時,脫離線程可以立即被中斷,而可連接線程則不可以。每個可連接線程必須在進程被允許可以退出的時候被串連。所以當線程處於周期性工作而不允許被中斷的時候,比如儲存資料到硬碟,可連接線程是最佳選擇。
如果你想要建立可連接線程,唯一的辦法是使用POSIX線程。POSIX預設建立的線程是可串連的。為了把線程標記為脫離的或可串連的,使用pthread_attr_setdetachstate函數來修改正在建立的線程的屬性。線上程啟動後,你可以通過調用pthread_detach函數來把線程修改為可串連的。關於更多POSIX線程函數資訊,參與pthread首頁。關於更多如果串連一個線程,參閱pthread_join的首頁。
4、設定線程的優先順序
你建立的任何線程預設的優先順序是和你本身線程相同。核心調度演算法在決定該運行那個線程時,把線程的優先順序作為考量因素,較高優先順序的線程會比較低優先順序的線程具有更多的運行機會。較高優先順序不保證你的線程具體執行的時間,只是相比較低優先順序的線程,它更有可能被調度器選擇執行而已。
重要:讓你的線程處於預設優先順序值是一個不錯的選擇。增加某些線程的優先順序,同時有可能增加了某些較低優先順序線程的饑餓程度。如果你的應用程式套件組合含較高優先順序和較低優先順序線程,而且它們之間必須互動,那麼較低優先順序的饑餓狀態有可能阻塞其他線程,並造成效能瓶頸。
如果你想改變線程的優先順序,Cocoa和POSIX都提供了一種方法來實現。對於Cocoa線程而言,你可以使用NSThread的setThreadPriority:類方法來設定當前運行線程的優先順序。對於POSIX線程,你可以使用pthread_setschedparam函數來實現。關於更多資訊,參與NSThread Class Reference或pthread_setschedparam首頁。
四、編寫你線程的主體進入點
對於大部分而言,Mac OS X上麵線程結構的主體進入點和其他平台基本一樣。你需要初始化你的資料結構,做一些工作或可行的設定一個run loop,並線上程代碼被執行完後清理它。根據設計,當你寫主體進入點的時候有可能需要採取一些額外的步驟。
1、建立一個自動釋放池
在Objective – C架構連結的應用程式,通常在它們的每一個線程必須建立至少一個自動釋放池。如果應用程式使用管理模型,即應用程式處理的retain和release對象,那麼自動釋放池捕獲任何從該線程autorelease的對象。
如果應用程式使用的記憶體回收機制,而不是管理的記憶體模型,那麼建立一個自動釋放池不是絕對必要的。在記憶體回收的應用程式裡面,一個自動釋放池是無害的,而且大部分情況是被忽略。允許通過個代碼管理必須同時支援記憶體回收和記憶體管理模型。在這種情況下,記憶體管理模型必須支援自動釋放池,當應用程式運行記憶體回收的時候,自動釋放池只是被忽略而已。
如果你的應用程式使用記憶體管理模型,在你編寫線程主體入口的時候第一件事情就是建立一個自動釋放池。同樣,在你的線程最後應該銷毀該自動釋放池。該池保證自動釋放。雖然對象被調用,但是它們不被release直到線程退出。
- (*pool = [[NSAutoreleasePool alloc] init]; [pool release]; }
因為進階的自動釋放池不會釋放它的對象直到線程退出。長時啟動並執行線程需求建立額外的自動釋放池來更頻繁的釋放它的對象。比如,一個使用run loop的線程可能在每次運行完一次迴圈的時候建立並釋放該自動釋放池。更頻繁的釋放對象可以防止你的應用程式記憶體佔用太大造成效能問題。雖然對於任何與效能相關的行為,你應該測量你代碼的實際表現,並適當地調整使用自動釋放池。
關於更多記憶體管理的資訊和自動釋放池,參閱“記憶體進階管理編程指南(Advanced Memory Management Programming Guide)”。
2、設定異常處理
如果你的應用程式捕獲並處理異常,那麼你的線程代碼應該時刻準備捕獲任何可能發生的異常。雖然最好的辦法是在異常發生的地方捕獲並處理它,但是如果在你的線程裡面捕獲一個拋出的異常失敗的話有可能造成你的應用程式強退。在你線程的主體進入點安裝一個try/catch模組,可以讓你捕獲任何未知的異常,並提供一個合適的響應。
當在Xcode構建你項目的時候,你可以使用C++或者Objective-C的異常處理風格。 關於更多設定如何在Objective-C裡面拋出和捕獲異常的資訊,參閱Exception Programming Topics。
3、設定一個Run Loop
當你想編寫一個獨立啟動並執行線程時,你有兩種選擇。第一種選擇是寫代碼作為一個長期的任務,很少甚至不中斷,線程完成的時候退出。第二種選擇是把你的線程放入一個迴圈裡面,讓它動態處理到來的工作要求。第一種方法不需要在你的代碼指定任何東西;你只需要啟動的時候做你打算做的事情即可。然而第二種選擇需要在你的線程裡面添加一個run loop。
Mac OS X和iOS提供了在每個線程實現run loop內建支援。Cocoa、Carbon和UIKit自動在你應用程式的主線程啟動一個run loop,但是如果你建立任何輔助線程,你必須手工的設定一個run loop並啟動它。
4、 中斷線程
退出一個線程推薦的方法是讓它在它主體進入點正常退出。經管Cocoa、POSIX和Multiprocessing Services提供了直接殺死線程的常式,但是使用這些常式是強烈不鼓勵的。殺死一個線程阻止了線程本身的清理工作。線程分配的記憶體可能造成泄露,並且其他線程當前使用的資源可能沒有被正確清理乾淨,之後造成潛在的問題。
如果你的應用程式需要在一個操作中間中斷一個線程,你應該設計你的線程響應取消或退出的訊息。對於長時啟動並執行操作,這意味著周期性停止工作來檢查該訊息是否到來。如果該訊息的確到來並要求線程退出,那麼線程就有機會來執行任何清理和退出工作;否則,它返回繼續工作和處理下一個資料區塊。
響應取消訊息的一個方法是使用run loop的輸入源來接收這些訊息。下面顯示了該結構的類似代碼在你的線程的主體入口裡面是怎麼樣的(該樣本顯示了主迴圈部分,不包括設立一個自動釋放池或配置實際的工作步驟)。該樣本在run loop上面安裝了一個自訂的輸入源,它可以從其他線程接收訊息。。執行工作的總和的一部分後,線程啟動並執行run loop來查看是否有訊息抵達輸入源。如果沒有,run loop立即退出,並且迴圈繼續處理下一個資料區塊。因為該處理器並沒有直接的訪問exitNow局部變數,允出準則是通過線程的字典來傳輸的。
- (==* runLoop = NSMutableDictionary* threadDict = (moreWorkToDo && ! exitNow = [[threadDict valueForKey: