線程管理

zz:

http://www.dreamingwish.com/dream-2012/ios-multi-threaded-programming-the-thread-management.html


Mac OS X和iOS裏面的每個進程都是有一個或多個線程構成,每個線程都代表一個代碼的執行路徑。每個應用程序啓動時候都是一個線程,它執行程序的main函數。應用程序可以生成額外的線程,其中每個線程執行一個特定功能的代碼。

當應用程序生成一個新的線程的時候,該線程變成應用程序進程空間內的一個實體。每個線程都擁有它自己的執行堆棧,由內核調度獨立的運行時間片。一個線程可以和其他線程或其他進程通信,執行I/O操作,甚至執行任何你想要它完成的任務。因爲它們處於相同的進程空間,所以一個獨立應用程序裏面的所有線程共享相同的虛擬內存空間,並且具有和進程相同的訪問權限。

本章提供了Mac OS X和iOS上面可用線程技術的預覽,並給出瞭如何在你的應用程序裏面使用它們的例子。

注意:獲取關於Mac OS上面線程架構,或者更多關於線程的背景資料。請參閱技術說明TN2028 –“線程架構”。

1.1        線程成本

多線程會佔用你應用程序(和系統的)的內存使用和性能方面的資源。每個線程都需要分配一定的內核內存和應用程序內存空間的內存。管理你的線程和協調其調度所需的核心數據結構存儲在使用Wired Memory的內核裏面。你線程的堆棧空間和每個線程的數據都被存儲在你應用程序的內存空間裏面。這些數據結構裏面的大部分都是當你首次創建線程或者進程的時候被創建和初始化的,它們所需的代價成本很高,因爲需要和內核交互。

表2-1量化了在你應用程序創建一個新的用戶級線程所需的大致成本。這些成本里面的部分是可配置的,比如爲輔助線程分配堆棧空間的大小。創建一個線程所需的時間成本是粗略估計的,僅用於當互相比較的時候。線程創建時間很大程度依賴於處理器的負載,計算速度,和可用的系統和程序空間。

Table 2-1  Thread creation costs

Item

Approximate cost

Notes

Kernel data structures

Approximately 1 KB

This memory is used to store the thread data structures and attributes, much of which is allocated as wired memory and therefore cannot be paged to disk.

Stack space

512 KB (secondary threads)

8 MB (Mac OS X main thread)

1 MB (iOS main thread)

The minimum allowed stack size for secondary threads is 16 KB and the stack size must be a multiple of 4 KB. The space for this memory is set aside in your process space at thread creation time, but the actual pages associated with that memory are not created until they are needed.

Creation time

Approximately 90 microseconds

This value reflects the time between the initial call to create the thread and the time at which the thread’s entry point routine began executing. The figures were determined by analyzing the mean and median values generated during thread creation on an Intel-based iMac with a 2 GHz Core Duo processor and 1 GB of RAM running Mac OS X v10.5.

注意:因爲底層內核的支持,操作對象(Operation objectis)可能創建線程更快。它們使用內核裏面常駐線程池裏面的線程來節省創建的時間,而不是每次都創建新的線程。關於更多使用操作對象(Operation objects)的信息,參閱併發編程指南(Concurrency Programming Guide)。

當編寫線程代碼時另外一個需要考慮的成本是生產成本。設計一個線程應用程序有時會需要根本性改變你應用程序數據結構的組織方式。要做這些改變可能需要避免使用同步,因爲本身設計不好的應用可能會造成巨大的性能損失。設計這些數據結構和在線程代碼裏面調試問題會增加開發一個線程應用所需的時間。然而避免這些消耗的話,可能在運行時候帶來更大的問題,如果你的多線程花費太多的時間在鎖的等待而沒有做任何事情。

1.1        創建一個線程

創建低級別的線程相對簡單。在所有情況下,你必須有一個函數或方法作爲線程的主入口點,你必須使用一個可用的線程例程啓動你的線程。以下幾個部分介紹了比較常用線程創建的基本線程技術。線程創建使用了這些技術的繼承屬性的默認設置,由你所使用的技術來決定。關於更多如何配置你的線程的信息,參閱“線程屬性配置”部分。

1.1.1    使用NSThread

使用NSThread來創建線程有兩個可以的方法:

  1. 使用detachNewThreadSelector:toTarget:withObject:類方法來生成一個新的線程。
  2. 創建一個新的NSThread對象,並調用它的start方法。(僅在iOS和Mac OS X v10.5及其之後才支持)

這兩種創建線程的技術都在你的應用程序裏面新建了一個脫離的線程。一個脫離的線程意味着當線程退出的時候線程的資源由系統自動回收。這也同樣意味着之後不需要在其他線程裏面顯式的連接(join)。因爲detachNewThreadSelctor:toTarget:withObject:方法在Mac OS X的任何版本都支持,所以在Cocoa應用裏面使用多線程的地方經常可以發現它。爲了生成一個新的線程,你只要簡單的提供你想要使用爲線程主體入口的方法的名稱(被指定爲一個selector),和任何你想在啓動時傳遞給線程的數據。下面的示例演示了這種方法的基本調用,來使用當前對象的自定義方法來生成一個線程。

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

在Mac OS X v10.5之前,你使用NSThread類來生成多線程。雖然你可以獲取一個NSThread對象並訪問線程的屬性,但你只能在線程運行之後在其內部做到這些。在Mac OS X v10.5支持創建一個NSThread對象,而無需立即生成一個相應的新線程(這些在iOS裏面同樣可用)。新版支持使得在線程啓動之前獲取並設置線程的很多屬性成爲可能。這也讓用線程對象來引用正在運行的線程成爲可能。

在Mac OS X v10.5及其之後初始化一個NSThread對象的簡單方法是使用initWithTarget:selector:object:方法。該方法和detachNewThreadSelector:toTarget:withObject:方法來初始化一個新的NSThread實例需要相同的額外開銷。然而它並沒有啓動一個線程。爲了啓動一個線程,你可以顯式調用先對象的start方法,如下面代碼:

NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                        selector:@selector(myThreadMainMethod:)
                                        object:nil];
[myThread start];  // Actually create the thread
 

注意:使用initWithTarget:selector:object:方法的替代辦法是子類化NSThread,並重寫它的main方法。你可以使用你重寫的該方法的版本來實現你線程的主體入口。更多信息,請參閱NSThread Class Reference裏面子類化的提示。

如果你擁有一個NSThread對象,它的線程當前真正運行,你可以給該線程發送消息的唯一方法是在你應用程序裏面的任何對象使用performSelector:onThread:withObject:waitUntilDone:方法。在Mac OS X v10.5支持在多線程上面執行selectors(而不是在主線程裏面),並且它是實現線程間通信的便捷方法。你使用該技術時所發送的消息會被其他線程作爲run-loop主體的一部分直接執行(當然這些意味着目標線程必須在它的run loop裏面運行,參閱“ Run Loops”)。當你使用該方法來實現線程通信的時候,你可能仍然需要一個同步操作,但是這比在線程間設置通信端口簡單多了。

注意:雖然在線程間的偶爾通信的時候使用該方法很好,但是你不能週期的或頻繁的使用performSelector:onThread:withObject:waitUntilDone:來實現線程間的通信。

關於線程間通信的可選方法,參閱“設置線程的脫離狀態”部分。

1.1.2    使用POSIX的多線程

Mac OS X和iOS提供基於C語言支持的使用POSIX線程API來創建線程的方法。該技術實際上可以被任何類型的應用程序使用(包括Cocoa和Cocoa Touch的應用程序),並且如果你當前真爲多平臺開發應用的話,該技術可能更加方便。你使用來創建線程的POSIX例程被調用的時候,使用pthread_create剛好足夠。

列表2-1顯示了兩個使用POSIX來創建線程的自定義函數。LaunchThread函數創建了一個新的線程,該線程的例程由PosixThreadMainRoutine函數來實現。因爲POSIX創建的線程默認情況是可連接的(joinable),下面的例子改變線程的屬性來創建一個脫離的線程。把線程標記爲脫離的,當它退出的時候讓系統有機會立即回收該線程的資源。

Listing 2-1  Creating a thread in C

#include  <assert.h>

#include  <pthread.h>

void* PosixThreadMainRoutine(void* data)
{
    // Do some work here.
    return NULL;
}

void LaunchThread()
{
    // Create the thread using POSIX routines.
    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)
    {
         // Report an error.
    }
}
 

    如果你把上面列表的代碼添加到你任何一個源文件,並且調用LaunchThread函數,它將會在你的應用程序裏面創建一個新的脫離線程。當然,新創建的線程使用該代碼沒有做任何有用的事情。線程將會加載並立即退出。爲了讓它更有興趣,你需要添加代碼到PosixThreadMainRoutine函數裏面來做一些實際的工作。爲了保證線程知道該幹什麼,你可以在創建的時候給線程傳遞一個數據的指針。把該指針作爲pthread_create的最後一個參數。

爲了在新建的線程裏面和你應用程序的主線程通信,你需要建立一條和目標線程之間的穩定的通信路徑。對於基於C語言的應用程序,有幾種辦法來實現線程間的通信,包括使用端口(ports),條件(conditions)和共享內存(shared memory)。對於長期存在的線程,你應該幾乎總是成立某種線程間的通信機制,讓你的應用程序的主線程有辦法來檢查線程的狀態或在應用程序退出時乾淨關閉它。

關於更多介紹POSIX線程函數的信息,參閱pthread的主頁。

1.1.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。關於更是介紹如果配置線程的信息,參閱“配置線程屬性”部分。

1.1.4    使用其他線程技術

儘管POSIX例程和NSThread類被推薦使用來創建低級線程,但是其他基於C語言的技術在Mac OS X上面同樣可用。在這其中,唯一一個可以考慮使用的是多處理服務(Multiprocessing Services),它本身就是在POSIX線程上執行。多處理服務是專門爲早期的Mac OS版本開發的,後來在Mac OS X裏面的Carbon應用程序上面同樣適用。如果你有代碼真是有該技術,你可以繼續使用它,儘管你應該把這些代碼轉化爲POSIX。該技術在iOS上面不可用。

關於更多如何使用多處理服務的信息,參閱多處理服務編程指南(Multiprocessing Services Programming Guide)

1.1.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.2        配置線程屬性

創建線程之後,或者有時候是之前,你可能需要配置不同的線程環境。以下部分描述了一些你可以做的改變,和在什麼時候你需要做這些改變。

1.2.1    配置線程的堆棧大小

對於每個你新創建的線程,系統會在你的進程空間裏面分配一定的內存作爲該線程的堆棧。該堆棧管理堆棧幀,也是任何線程局部變量聲明的地方。給線程分配的內存大小在“線程成本”裏面已經列舉了。

如果你想要改變一個給定線程的堆棧大小,你必須在創建該線程之前做一些操作。所有的線程技術提供了一些辦法來設置線程堆棧的大小。雖然可以使用NSThread來設置堆棧大小,但是它只能在iOS和Mac OS X v10.5及其之後纔可用。表2-2列出了每種技術的對於不同的操作。

Table 2-2  Setting the stack size of a thread

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.

1.2.2    配置線程本地存儲

每個線程都維護了一個鍵-值的字典,它可以在線程裏面的任何地方被訪問。你可以使用該字典來保存一些信息,這些信息在整個線程的執行過程中都保持不變。比如,你可以使用它來存儲在你的整個線程過程中Run loop裏面多次迭代的狀態信息。

Cocoa和POSIX以不同的方式保存線程的字典,所以你不能混淆並同時調用者兩種技術。然而只要你在你的線程代碼裏面堅持使用了其中一種技術,最終的結果應該是一樣的。在Cocoa裏面,你使用NSThread的threadDictionary方法來檢索一個NSMutableDictionary對象,你可以在它裏面添加任何線程需要的鍵。在POSIX裏面,你使用pthread_setspecific和pthread_getspecific函數來設置和訪問你線程的鍵和值。

1.2.3    設置線程的脫離狀態

大部分上層的線程技術都默認創建了脫離線程(Datached thread)。大部分情況下,脫離線程(Detached thread)更受歡迎,因爲它們允許系統在線程完成的時候立即釋放它的數據結構。脫離線程同時不需要顯示的和你的應用程序交互。意味着線程檢索的結果由你來決定。相比之下,系統不回收可連接線程(Joinable thread)的資源直到另一個線程明確加入該線程,這個過程可能會阻止線程執行加入。

你可以認爲可連接線程類似於子線程。雖然你作爲獨立線程運行,但是可連接線程在它資源可以被系統回收之前必須被其他線程連接。可連接線程同時提供了一個顯示的方式來把數據從一個正在退出的線程傳遞到其他線程。在它退出之前,可連接線程可以傳遞一個數據指針或者其他返回值給pthread_exit函數。其他線程可以通過pthread_join函數來拿到這些數據。

重要:在應用程序退出時,脫離線程可以立即被中斷,而可連接線程則不可以。每個可連接線程必須在進程被允許可以退出的時候被連接。所以當線程處於週期性工作而不允許被中斷的時候,比如保存數據到硬盤,可連接線程是最佳選擇。

如果你想要創建可連接線程,唯一的辦法是使用POSIX線程。POSIX默認創建的線程是可連接的。爲了把線程標記爲脫離的或可連接的,使用pthread_attr_setdetachstate函數來修改正在創建的線程的屬性。在線程啓動後,你可以通過調用pthread_detach函數來把線程修改爲可連接的。關於更多POSIX線程函數信息,參與pthread主頁。關於更多如果連接一個線程,參閱pthread_join的主頁。

1.2.4    設置線程的優先級

你創建的任何線程默認的優先級是和你本身線程相同。內核調度算法在決定該運行那個線程時,把線程的優先級作爲考量因素,較高優先級的線程會比較低優先級的線程具有更多的運行機會。較高優先級不保證你的線程具體執行的時間,只是相比較低優先級的線程,它更有可能被調度器選擇執行而已。

重要:讓你的線程處於默認優先級值是一個不錯的選擇。增加某些線程的優先級,同時有可能增加了某些較低優先級線程的飢餓程度。如果你的應用程序包含較高優先級和較低優先級線程,而且它們之間必須交互,那麼較低優先級的飢餓狀態有可能阻塞其他線程,並造成性能瓶頸。

如果你想改變線程的優先級,Cocoa和POSIX都提供了一種方法來實現。對於Cocoa線程而言,你可以使用NSThread的setThreadPriority:類方法來設置當前運行線程的優先級。對於POSIX線程,你可以使用pthread_setschedparam函數來實現。關於更多信息,參與NSThread Class Reference或pthread_setschedparam主頁。

1.3        編寫你線程的主體入口點

對於大部分而言,Mac OS X上面線程結構的主體入口點和其他平臺基本一樣。你需要初始化你的數據結構,做一些工作或可行的設置一個run loop,並在線程代碼被執行完後清理它。根據設計,當你寫的主體入口點的時候有可能需要採取一些額外的步驟。

1.3.1    創建一個自動釋放池(Autorelease Pool)

在Objective – C框架鏈接的應用程序,通常在它們的每一個線程必須創建至少一個自動釋放池。如果應用程序使用管理模型,即應用程序處理的retain和release對象,那麼自動釋放池捕獲任何從該線程autorelease的對象。

如果應用程序使用的垃圾回收機制,而不是管理的內存模型,那麼創建一個自動釋放池不是絕對必要的。在垃圾回收的應用程序裏面,一個自動釋放池是無害的,而且大部分情況是被忽略。允許通過個代碼管理必須同時支持垃圾回收和內存管理模型。在這種情況下,內存管理模型必須支持自動釋放池,當應用程序運行垃圾回收的時候,自動釋放池只是被忽略而已。

如果你的應用程序使用內存管理模型,在你編寫線程主體入口的時候第一件事情就是創建一個自動釋放池。同樣,在你的線程最後應該銷燬該自動釋放池。該池保證自動釋放。雖然對象被調用,但是它們不被release直到線程退出。列表2-2顯示了線程主體入口使用自動釋放池的基本結構。

Listing 2-2  Defining your thread entry point routine

- (void)myThreadMainRoutine
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool
    // Do thread work here.
    [pool release];  // Release the objects in the pool.
}
 

    因爲高級的自動釋放池不會釋放它的對象直到線程退出。長時運行的線程需求新建額外的自動釋放池來更頻繁的釋放它的對象。比如,一個使用run loop的線程可能在每次運行完一次循環的時候創建並釋放該自動釋放池。更頻繁的釋放對象可以防止你的應用程序內存佔用太大造成性能問題。雖然對於任何與性能相關的行爲,你應該測量你代碼的實際表現,並適當地調整使用自動釋放池。

關於更多內存管理的信息和自動釋放池,參閱“內存高級管理編程指南(Advanced Memory Management Programming Guide)”。

1.3.2    設置異常處理

如果你的應用程序捕獲並處理異常,那麼你的線程代碼應該時刻準備捕獲任何可能發生的異常。雖然最好的辦法是在異常發生的地方捕獲並處理它,但是如果在你的線程裏面捕獲一個拋出的異常失敗的話有可能造成你的應用程序強退。在你線程的主體入口點安裝一個try/catch模塊,可以讓你捕獲任何未知的異常,並提供一個合適的響應。

當在Xcode構建你項目的時候,你可以使用C++或者Objective-C的異常處理風格。 關於更多設置如何在Objective-C裏面拋出和捕獲異常的信息,參閱Exception Programming Topics。

1.3.3    設置一個Run Loop

當你想編寫一個獨立運行的線程時,你有兩種選擇。第一種選擇是寫代碼作爲一個長期的任務,很少甚至不中斷,線程完成的時候退出。第二種選擇是把你的線程放入一個循環裏面,讓它動態的處理到來的任務請求。第一種方法不需要在你的代碼指定任何東西;你只需要啓動的時候做你打算做的事情即可。然而第二種選擇需要在你的線程裏面添加一個run loop。

Mac OS X和iOS提供了在每個線程實現run loop內置支持。Cocoa、Carbon和UIKit自動在你應用程序的主線程啓動一個run loop,但是如果你創建任何輔助線程,你必須手工的設置一個run loop並啓動它。

關於更多使用和配置run loop的信息,參閱“Run Loops”部分。

1.4        中斷線程

退出一個線程推薦的方法是讓它在它主體入口點正常退出。經管Cocoa、POSIX和Multiprocessing Services提供了直接殺死線程的例程,但是使用這些例程是強烈不鼓勵的。殺死一個線程阻止了線程本身的清理工作。線程分配的內存可能造成泄露,並且其他線程當前使用的資源可能沒有被正確清理乾淨,之後造成潛在的問題。

如果你的應用程序需要在一個操作中間中斷一個線程,你應該設計你的線程響應取消或退出的消息。對於長時運行的操作,這意味着週期性停止工作來檢查該消息是否到來。如果該消息的確到來並要求線程退出,那麼線程就有機會來執行任何清理和退出工作;否則,它返回繼續工作和處理下一個數據塊。

響應取消消息的一個方法是使用run loop的輸入源來接收這些消息。列表2-3顯示了該結構的類似代碼在你的線程的主體入口裏面是怎麼樣的(該示例顯示了主循環部分,不包括設立一個自動釋放池或配置實際的工作步驟)。該示例在run loop上面安裝了一個自定義的輸入源,它可以從其他線程接收消息。關於更多設置輸入源的信息,參閱“配置Run Loop源”。執行工作的總和的一部分後,線程運行的run loop來查看是否有消息抵達輸入源。如果沒有,run loop立即退出,並且循環繼續處理下一個數據塊。因爲該處理器並沒有直接的訪問exitNow局部變量,退出條件是通過線程的字典來傳輸的。

Listing 2-3  Checking for an exit condition during a long job

- (void)threadMainRoutine
{
    BOOL moreWorkToDo = YES;
    BOOL exitNow = NO;
    NSRunLoop* runLoop = [NSRunLoop currentRunLoop];

    // Add the exitNow BOOL to the thread dictionary.
    NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
    [threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];

    // Install an input source.
    [self myInstallCustomInputSource];

    while (moreWorkToDo && !exitNow)
    {
        // Do one chunk of a larger body of work here.
        // Change the value of the moreWorkToDo Boolean when done.

        // Run the run loop but timeout immediately if the input source isn't waiting to fire.
        [runLoop runUntilDate:[NSDate date]];

        // Check to see if an input source handler changed the exitNow value.
        exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
    }
}
發佈了19 篇原創文章 · 獲贊 6 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章