天天看點

線程程式設計指南翻譯第二篇(線程管理)線程管理

文檔位址

示例代碼下載下傳

線程管理

在OS X和iOS系統中每個程序(應用程式)都有一個或者多個線程構成,每個線程表示着執行應用程式代碼的單個路徑。每個應用程式都以單線程啟動,這個線程運作應用程式的main函數。應用程式能夠生成一些執行特定函數代碼的額外線程。

當應用程式生成一個新的線程,該線程在應用程式的程序空間裡成為一個獨立的實體。每個線程都有自己的執行堆棧,并由核心單獨排程運作時。線程可以與其他線程和其他程序通信,執行I / O操作,并執行您可能需要執行的任何操作。由于它們位于同一程序空間内,是以單個應用程式中的所有線程共享相同的虛拟記憶體空間,并具有與程序本身相同的通路權限。

線程成本

線程在記憶體使用和性能方面對您的程式(和系統)來說是一個真正的成本。每個線程都需要在核心記憶體空間和程式的記憶體空間中配置設定記憶體。其核心結構需要使用有線記憶體去管理線程和派發存儲在核心中的坐标。線程的堆棧空間和每個線程的資料存儲在程式的記憶體空間中。大多數這些結構都是在您第一次建立線程時建立和初始化的 - 由于與核心的必要互動,該過程可能相對昂貴。其中一些成本是可配置的,例如為輔助線程配置設定的堆棧空間量。建立線程的時間成本是粗略的近似值,應該僅用于互相比較。線程建立時間可能會有很大差異,具體取決于處理器負載,計算機速度以及可用系統和程式記憶體的數量。

表2-1 線程建立成本

條目 近似成本 說明
核心資料結構 大約1 KB 此記憶體用于存儲線程資料結構和屬性,其中大部分被配置設定為有線記憶體,是以無法分頁到磁盤。
堆棧空間 512 KB(輔助線程)8 MB(OS X主線程)1 MB(iOS主線程) 輔助線程允許的最小堆棧大小為16 KB,堆棧大小必須為4 KB的倍數。建立線程時在程序空間中會為線程留出此記憶體空間,但是實際的記憶體位址并不是線上程建立的時候關聯上的,而是在需要的時候。
建立時間 大約90微秒 這個值反映的時間是從線程建立的初始調用到線程入口點的例程開始執行之間的時間。這些資料是通過分析在基于因特爾的iMac上使用2 GHz Core Duo處理器和運作于OS X v10.5上的1 GB RAM建立線程期間生成的平均值和中值來确定的。

注意:由于底層核心的支援,operation objects通常能夠更快的建立線程。他們不是每次都從頭開始建立線程,而是使用已駐留在核心中的線程池來節省配置設定時間。有關使用操作對象的更多資訊,請參閱“ 并發程式設計指南”。

編寫線程代碼時要考慮的另一個成本是生産成本。設計線程化應用程式有時可能需要對組織應用程式資料結構的方式進行根本性更改。進行這些更改以避免使用同步可能是必要的,這本身可能會對設計不佳的應用程式造成巨大的性能損失。設計這些資料結構以及調試線程代碼中的問題可能會增加開發多線程應用程式所需的時間。如果線程花費太多時間等待鎖定或什麼都不做,然而避免這些成本可能會在runtime中産生更大的問題。

建立一個線程

建立低級線程相對簡單。在所有情況下,您必須具有一個函數或方法來充當線程的主入口點,并且必須使用一個可用的線程例程來啟動您的線程。以下部分顯示了比較常用的線程技術的基本建立過程。使用這些技術建立的線程會繼承一組預設屬性,這些屬性由使用的技術決定。有關如何配置線程的資訊,請參閱配置線程屬性。

使用NSThread

使用NSThread類建立線程有兩種方法:

  • 使用detachNewThreadSelector:toTarget:withObject:類方法生成新線程。
  • 建立一個新的NSThread對象并調用其start方法。(僅在iOS和OS X v10.5及更高版本中受支援。)

這兩種技術都會在應用程式中建立一個分離的線程 分離線程意味着線程退出時系統會自動回收線程的資源。這也意味着您的代碼以後不必明确地與該線程連接配接。

因為OS X的所有版本都支援detachNewThreadSelector:toTarget:withObject:該方法,是以通常能在使用線程的現有Cocoa應用程式中找到它。要分離新線程,隻需提供要用作線程入口點的方法名稱(指定為選擇器),定義該方法的對象以及要在啟動時傳遞給線程的任何資料。以下示例顯示了此方法的基本調用,該方法使用目前對象的自定義方法生成線程。

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

在OS X v10.5之前,主要使用NSThread該類來生成線程。雖然您可以擷取一個NSThread對象并通路某些線程屬性,但您隻能線上程運作後從線程本身執行此操作。在OS X v10.5中,添加了對建立NSThread對象的支援,而不會立即生成相應的新線程。(此支援也可在iOS中使用。)此支援使得在啟動線程之前擷取和設定各種線程屬性成為可能。它還可以使用該線程對象以後引用正在運作的線程。

NSThread在OS X v10.5及更高版本中初始化NSThread對象的簡單方法是使用initWithTarget:selector:object:方法。此方法與detachNewThreadSelector:toTarget:withObject:方法擷取完全相同的資訊,并使用它來初始化新NSThread執行個體。但是,它不會啟動該線程。要啟動該線程,請顯式調用線程對象的start方法,如以下示例所示:

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(myThreadMainMethod:) object:nil];
    [thread start];           

注意: 使用該initWithTarget:selector:object:方法的另一種方法是子類化NSThread并覆寫其main方法。您将使用此方法的重寫版本來實作線程的主入口點。有關更多資訊,請參閱NSThread類參考中的子類注釋。

如果您有一個目前正在運作線程的NSThread對象,則應用程式中幾乎任何對象可以将消息發送到該線程的一種方法是使用performSelector:onThread:withObject:waitUntilDone:方法。支援線上程(主線程之外)上執行選擇器方法是在OS X v10.5中引入的,這是線上程之間進行通信的便捷方式。此支援也可在iOS中使用。)這種技術發送的消息被其他線程作為正常runloop處理的一部分直接執行。(當然這意味着目标線程已經在它的runloop中運作着,請參閱runloop。)以這種方式進行通信仍然需要某種形式的同步,但是這比線上程之間設定通信端口簡單。

與其他線程通信選項清單,請參閱設定線程的分離狀态。

使用POSIX線程

OS X 和 iOS使用POSIX線程API為建立線程提供基于C的支援。這種技術實際上可以用于任何類型的應用程式(包括Cocoa和Cocoa Touch應用程式),編寫跨平台軟體更加友善。POSIX例程用于建立線程,足夠恰當地調用pthread_create函數。

清單2-1顯示了兩個使用POSIX調用建立線程的自定義函數。該LaunchThread函數建立一個新的線程,其主程式在PosixThreadMainRoutine函數中實作。因為預設情況下POSIX将線程建立為可連接配接,是以此示例更改線程的屬性以建立分離線程。将線程标記為已分離使系統有機會在退出時立即回收該線程的資源。

清單2-1 在C中建立一個線程

void *PosixThreadMainOutine(void *data) {
    //在這做一些工作

    return NULL;
}
void LaunchThread(void *data) {
    //使用POSIX例程建立線程
    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, posixThreadMainOutine, data);

    returnVal = pthread_attr_destroy(&attr);
    assert(!returnVal);
    if (threadError != 0) {
        //抛出錯誤

    }
}           

如果将前面清單中的代碼添加到其中一個源檔案并調用該LaunchThread函數,它将在您的應用程式中建立一個新的分離線程。當然,使用此代碼建立的新線程不會做任何有用的事情。線程将啟動并幾乎立即退出。為了使事情更有趣,您需要向PosixThreadMainRoutine函數添加代碼以執行一些實際工作。為了確定線程知道要做什麼工作,您可以在建立時向其傳遞指向某些資料的指針。您将此指針作為pthread_create函數的最後一個參數傳遞。

要将新建立的線程中的資訊傳遞回應用程式的主線程,您需要在目标線程之間建立通信路徑。對于基于C的應用程式,有多種方法可以線上程之間進行通信,包括使用端口,條件或共享記憶體。對于長期存在的線程,您幾乎應該始終設定某種線程間通信機制,以便為應用程式的主線程提供一種方法來檢查線程的狀态,或者在應用程式退出時将其關閉。

有關POSIX線程函數的更多資訊,請參見pthread手冊頁。

使用NSObject生成線程

在iOS和OS X v10.5及更高版本中,所有對象都能夠生成新線程并使用它來執行其中一個方法。performSelectorInBackground:withObject:方法建立一個新的分離線程,并使用指定的方法作為新線程的入口點。例如,如果您有一些對象(由myObj變量表示)并且該對象具有想要在背景線程中運作的doSomething方法,則可以使用以下代碼執行此操作:

[myObj performSelectorInBackground:@selector(doSomething)withObject:nil];           

與用目前對象,選擇器方法和參數對象作為參數調用NSThread的detachNewThreadSelector:toTarget:withObject:方法調用此方法的效果是一樣的。使用預設配置立即生成新線程并開始運作。在選擇器内部,您必須像處理任何線程一樣配置線程。例如,您需要設定自動釋放池(如果您沒有使用垃圾收集)并配置線程的運作循環(如果您計劃使用它)。有關如何配置新線程的資訊,請參閱配置線程屬性。

在Cocoa應用程式中使用POSIX線程

盡管NSThread該類是在Cocoa應用程式中建立線程的主要接口,如果這樣做更友善的話,依然可以自由的使用POSIX線程。例如,如果已經擁有使用它們的代碼并且不想重寫它,則可以使用POSIX線程。如果計劃在Cocoa應用程式中使用POSIX線程,仍應該了解Cocoa和線程之間的互動,并遵守以下各節中的準則。

保護Cocoa架構

對于多線程應用程式,Cocoa架構使用鎖和其他形式的内部同步來確定它們的行為正确。但是,為了防止這些鎖在單線程情況下降低性能,Cocoa不會建立它們,直到應用程式使用NSThread該類生成其第一個新線程。如果僅使用POSIX線程例程生成線程,Cocoa不會收到它需要知道您的應用程式現在是多線程的通知。當發生這種情況時,涉及Cocoa架構的操作可能會使您的應用程式不穩定或崩潰。

為了讓Cocoa知道您打算使用多個線程,您所要做的就是使用NSThread類生成一個線程并讓該線程立即退出。你的線程入口點不需要做任何事情。隻是産生NSThread線程使用的行為足以確定Cocoa架構所需的鎖定到位。

如果您不确定Cocoa是否認為您的應用程式是多線程的,您可以使用NSThreaddeisMultiThreaded方法來檢查。

混合POSIX和Cocoa鎖

在同一個應用程式中使用POSIX和Cocoa鎖的混合是安全的。Cocoa鎖和條件對象基本上隻是POSIX互斥和條件的包裝器。但是,對于給定的鎖,必須始終使用相同的接口來建立和操作該鎖。換句話說,您不能使用Cocoa NSLock對象來操作使用該pthread_mutex_init函數建立的互斥鎖,反之亦然。

配置線程屬性

在建立線程之後,有時在之前,您可能希望配置線程環境的不同部分。以下部分介紹了您可以進行的一些更改以及何時可以進行的更改。

配置線程的堆棧大小

對于您建立的每個新線程,系統會在程序空間中配置設定特定數量的記憶體,以充當該線程的堆棧。堆棧管理堆棧幀,也是聲明線程的任何局部變量的地方。為線程配置設定的記憶體量列在“ 線程成本”中。

如果要更改給定線程的堆棧大小,則必須在建立線程之前執行此操作。盡管設定堆棧大小NSThread僅在iOS和OS X v10.5及更高版本中可用,但所有線程技術都提供了一些設定堆棧大小的方法。表2-2列出了每種技術的不同選項。

表2-2 設定線程的堆棧大小

技術 選項
Cocoa 在iOS和OS X v10.5及更高版本中,配置設定并初始化NSThread對象(不要使用該detachNewThreadSelector:toTarget:withObject:方法)。在調用start線程對象的方法之前,請使用該setStackSize:方法指定新的堆棧大小。
POSIX 建立一個新pthread_attr_t結構并使用該pthread_attr_setstacksize函數更改預設堆棧大小。pthread_create建立線程時将屬性傳遞給函數。
多處理服務 MPCreateTask在建立線程時,将适當的堆棧大小值傳遞給函數。

配置線程局部存儲

每個線程都維護一個鍵值對字典,可以從線程中的任何位置通路。您可以使用此字典存儲要在整個線程執行期間保留的資訊。例如,您可以使用它來存儲您希望通過線程運作循環的多次疊代持久化的狀态資訊。

Cocoa和POSIX以不同的方式存儲線程字典,是以您無法混合和比對對這兩種技術的調用。但是,隻要您線上程代碼中堅持使用一種技術,最終結果應該是相似的。在Cocoa中,您使用NSThread對象的threadDictionary方法來檢索NSMutableDictionary對象,您可以向其添加線程所需的任何鍵。在POSIX中,您使用pthread_setspecific和pthread_getspecific函數來設定和擷取線程的鍵和值。

設定線程的分離狀态

大多數進階線程技術預設建立分離線程。在大多數情況下,首選分離線程是因為它們允許系統在完成線程後立即釋放線程的資料結構。分離的線程也不需要與您的程式進行明确的互動。意味着從線程中檢索結果的方法由您自行決定。相比之下,系統不會回收可連接配接線程的資源,直到另一個線程顯式加入該線程,這個程序可能會阻塞執行連接配接的線程。

您可以将可連接配接線程視為類似于子線程。盡管它們仍然作為獨立線程運作,但是在系統可以回收其資源之前,必須由另一個線程連接配接可連接配接線程。可連接配接線程還提供了一種将資料從現有線程傳遞到另一個線程的顯式方法。在它退出之前,可連接配接的線程可以将資料指針或其他傳回值傳遞給pthread_exit函數。然後另一個線程可以通過調用該pthread_join函數來聲明該資料。

重要說明: 在應用程式退出時,分離的線程可以立即終止,但可連接配接的線程不能。必須先連接配接每個可連接配接線程,然後才允許程序退出。是以,線上程正在執行不應被中斷的關鍵工作(例如将資料儲存到磁盤)的情況下,可連接配接線程可能是優選的。

如果您确實想要建立可連接配接的線程,唯一的方法是使用POSIX線程。預設情況下,POSIX将線程建立為可連接配接。要将線程标記為已分離或可連接配接,請pthread_attr_setdetachstate在建立線程之前使用該函數修改線程屬性。線程開始後,您可以通過調用該pthread_detach函數将可連接配接線程更改為分離線程。有關這些POSIX線程函數的更多資訊,請參見pthread手冊頁。有關如何加入線程的資訊,請參見pthread_join手冊頁。

設定線程優先級

您建立的任何新線程都具有與之關聯的預設優先級。核心的排程算法在确定要運作哪些線程時考慮線程優先級,優先級較高的線程比具有較低優先級的線程更可能運作。較高優先級并不能保證線程的特定執行時間,隻是與較低優先級的線程相比,排程程式更有可能選擇它。

重要提示: 将線程的優先級保留為預設值通常是個好主意。增加某些線程的優先級也會增加低優先級線程之間饑餓的可能性。如果您的應用程式包含必須互相互動的高優先級和低優先級線程,則較低優先級線程的饑餓可能會阻塞其他線程并産生性能瓶頸。

如果你想修改線程優先級,Cocoa和POSIX都提供了一種方法。對于Cocoa線程,您可以使用NSThread的setThreadPriority:class方法設定目前運作的線程的優先級。對于POSIX線程,您可以使用該pthread_setschedparam函數。有關更多資訊,請參見NSThread類參考或pthread_setschedparam手冊頁。

編寫線程入口例程

在大多數情況下,線程的入口點例程的結構在OS X中與在其他平台上的結構相同。初始化資料結構,執行某些操作或者可選地設定運作循環,并線上程代碼完成時進行清理。根據您的設計,在編寫入口例程時可能需要執行一些額外的步驟。

建立自動釋放池

在Objective-C架構中連結的應用程式通常必須在每個線程中建立至少一個自動釋放池。如果應用程式使用托管模型 - 應用程式處理保留和釋放對象的位置 - 自動釋放池将捕獲從該線程自動釋放的所有對象。

如果應用程式使用垃圾收集而不是托管記憶體模型,則不一定要建立自動釋放池。在垃圾收集的應用程式中存在自動釋放池是無害的,并且在很大程度上被忽略。允許代碼子產品必須同時支援垃圾收集和托管記憶體模型的情況。在這種情況下,必須存在自動釋放池以支援托管記憶體模型代碼,如果應用程式在啟用垃圾收集的情況下運作,則會被忽略。

如果您的應用程式使用托管記憶體模型,那麼建立自動釋放池應該是您線上程入口例程中首先要做的事情。同樣,銷毀這個自動釋放池應該是你線上程中做的最後一件事。此池確定捕獲自動釋放的對象,但線上程本身退出之前不會釋放它們。清單2-2顯示了使用自動釋放池的基本線程入口例程的結構。

清單2-2 定義線程入口點例程

- (void)myThreadMainRoutine
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 頂級池

    //線程在這裡工作

    [pool release];  // 釋放池中的對象
}           

由于頂級自動釋放池線上程退出之前不釋放其對象,是以長期存在的線程應建立其他自動釋放池以更頻繁地釋放對象。例如,使用運作循環的線程可能每次通過該循環建立并釋放自動釋放池。更頻繁地釋放對象可以防止應用程式的記憶體占用過大,進而導緻性能問題。與任何與性能相關的行為一樣,您應該測量代碼的實際性能并适當調整自動釋放池的使用。

有關記憶體管理和自動釋放池的詳細資訊,請參閱“ 進階記憶體管理程式設計指南”。

設定異常處理

如果您的應用程式捕獲并處理異常,則應準備好線程代碼以捕獲可能發生的任何異常。雖然最好在異常産生時處理異常,但是如果未能線上程中捕獲抛出異常會導緻應用程式退出。線上程入口例程中安裝最終的try / catch允許您捕獲任何未知異常并提供适當的響應。

在Xcode中建構項目時,可以使用C ++或Objective-C異常處理樣式。有關設定如何在Objective-C中引發和捕獲異常的資訊,請參閱異常程式設計主題。

設定運作循環

編寫要在單獨的線程上運作的代碼時,您有兩個選擇。第一種選擇是将線程的代碼編寫為一個很長的任務,在很少或沒有中斷的情況下執行,并在完成時讓線程退出。第二個選項是将您的線程放入循環中,并在它們到達時動态處理請求。第一個選項不需要為您的代碼進行特殊設定; 做你想做的工作就行了。但是,第二個選項涉及設定線程的運作循環。

OS X和iOS提供了在每個線程中實作運作循環的内置支援。應用程式架構自動啟動應用程式主線程的運作循環。如果建立任何輔助線程,則必須配置運作循環并手動啟動它。

有關使用和配置運作循環的資訊,請參閱運作循環。

終止線程

退出線程的推薦方法是讓它正常退出其入口點例程。盡管Cocoa,POSIX和Multiprocessing Services提供了直接殺死線程的例程,但強烈建議不要使用此類例程。殺死一個線程可以防止該線程自行清理。線程配置設定的記憶體可能會被洩露,并且線程目前正在使用的任何其他資源可能無法正确清理,進而在以後産生潛在問題。

如果您預計需要在操作過程中終止線程,則應該從一開始就設計線程以響應取消或退出消息。對于長時間運作的操作,這可能意味着定期停止工作并檢查是否有這樣的消息到達。如果确實有消息要求線程退出,則線程将有機會執行任何所需的清理并正常退出; 否則,它可以簡單地傳回工作并處理下一個資料塊。

響應取消消息的一種方法是使用運作循環輸入源來接收此類消息。清單2-3顯示了此代碼線上程主入口例程中的外觀結構。(該示例僅顯示主循環部分,不包括設定自動釋放池或配置要執行的實際工作的步驟。)該示例在運作循環上安裝自定義輸入源可能被另一個線程通知到; 有關設定輸入源的資訊,請參閱配置運作循環源。執行總工作量的一部分後,線程會短暫運作運作循環,以檢視是否有消息到達輸入源。如果沒有,則運作循環立即退出,循環繼續下一個工作塊。因為處理程式不能直接通路exitNow局部變量,是以退出條件通過線程字典中的鍵值對進行傳遞。

清單2-3 長時間工作檢查退出條件

- (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布爾值。

        //如果輸入源沒有等待觸發,則runLoop立即逾時
        [runLoop runUntilDate:[NSDate date]];

        //檢查輸入源處理程式是否更改了exitNow值
        exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
    }
}