iOS多線程編程指南(二)線程管理


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

 

一、線程成本

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

二、 創建一個線程

1、使用NSThread

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

  1. 使用detachNewThreadSelector:toTarget:withObject:類方法來生成一個新的線程。
    [NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil];

      //NSLog(@"%@", [NSThread currentThread]);

     

  2. 創建一個新的NSThread對象,並調用它的start方法。(僅在iOS和Mac OS X v10.5及其之后才支持)
    NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                            selector:@selector(myThreadMainMethod:)
                                            object:nil];
    [myThread start];  // Actually create the thread

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

2、使用POSIX的多線程

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

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

#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的主頁。

 

 

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直到線程退出。

- (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)”。

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局部變量,退出條件是通過線程的字典來傳輸的。

- (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];
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM