Cocoa Touch(四): 多線程GCD, NSObject, NSThread, NSOperationQueue

    多線程的重要性沒必要多言,現代操做系統不可能離開進程線程的抽象。具體到ios應用,咱們只能在一個進程中管理線程,主線程不該該去執行很是耗時間的後臺操做致使出現卡機現象,後臺的事情交給後臺線程來完成。ios

 

Grand Central Dispatch程序員

GCD組件包含兩大部分,第一個部分是提供了C語言線程同步互斥接口,第二個部分就是dispatch隊列。web

dispatch block的執行最終都會放進某個隊列中去進行,它相似NSOperationQueue但更復雜也更強大,而且能夠嵌套使用。因此說,結合block實現的GCD,把函數閉包(Closure)的特性發揮得淋漓盡致。數據庫

 

dispatch隊列的生成能夠有這幾種方式:編程

1. dispatch_queue_t queue = dispatch_queue_create("com.dispatch.serial", DISPATCH_QUEUE_SERIAL); //生成一個串行隊列,隊列中的block按照先進先出(FIFO)的順序去執行,實際上爲單線程執行。第一個參數是隊列的名稱,在調試程序時會很是有用,全部儘可能不要重名了。安全

2. dispatch_queue_t queue = dispatch_queue_create("com.dispatch.concurrent", DISPATCH_QUEUE_CONCURRENT); //生成一個併發執行隊列,block被分發到多個線程去執行網絡

3. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //得到程序進程缺省產生的併發隊列,可設定優先級來選擇高、中、低三個優先級隊列。因爲是系統默認生成的,因此沒法調用dispatch_resume()和dispatch_suspend()來控制執行繼續或中斷。須要注意的是,三個隊列不表明三個線程,可能會有更多的線程。併發隊列能夠根據實際狀況來自動產生合理的線程數,也可理解爲dispatch隊列實現了一個線程池的管理,對於程序邏輯是透明的。多線程

官網文檔解釋說共有三個併發隊列,但實際還有一個更低優先級的隊列,設置優先級爲DISPATCH_QUEUE_PRIORITY_BACKGROUND。Xcode調試時能夠觀察到正在使用的各個dispatch隊列。閉包

4. dispatch_queue_t queue = dispatch_get_main_queue(); //得到主線程的dispatch隊列,注意這個隊列中的任務須要被主線程完成。一樣沒法控制主線程dispatch隊列的執行繼續或中斷。併發

接下來咱們可使用dispatch_async或dispatch_sync函數來加載須要運行的block。

dispatch_async(queue, ^{

  //block具體代碼

}); //異步執行block,函數當即返回

dispatch_sync(queue, ^{

  //block具體代碼

}); //同步執行block,函數不返回,一直等到block執行完畢。編譯器會根據實際狀況優化代碼,因此有時候你會發現block其實還在當前線程上執行,並沒用產生新線程。

實際編程經驗告訴咱們,儘量避免使用dispatch_sync,嵌套使用時還容易引發程序死鎖。

若是queue1是一個串行隊列的話,這段代碼當即產生死鎖:

dispatch_sync(queue1, ^{

dispatch_sync(queue1, ^{

    ......

  });

  ......

 });

在主線程當中,爲何下面代碼也確定死鎖:

dispatch_sync(dispatch_get_main_queue(), ^{

  ......

});

 

那實際運用中,通常能夠用dispatch這樣來寫,常見的網絡請求數據多線程執行模型:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  //子線程中開始網絡請求數據

  //更新數據模型

  dispatch_sync(dispatch_get_main_queue(), ^{

    //在主線程中更新UI代碼

  });

});

程序的後臺運行和UI更新代碼緊湊,代碼邏輯一目瞭然。

 

dispatch隊列是線程安全的,能夠利用串行隊列實現鎖的功能。好比多線程寫同一數據庫,須要保持寫入的順序和每次寫入的完整性,簡單地利用串行隊列便可實現:

dispatch_queue_t queue1 = dispatch_queue_create("com.dispatch.writedb", DISPATCH_QUEUE_SERIAL);

- (void)writeDB:(NSData *)data

{

  dispatch_async(queue1, ^{

    //write database

  });

}

下一次調用writeDB:必須等到上次調用完成後才能進行,保證writeDB:方法是線程安全的。

 

dispatch隊列還實現其它一些經常使用函數,包括:

void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t)); //重複執行block,須要注意的是這個方法是同步返回,也就是說等到全部block執行完畢才返回,如需異步返回則嵌套在dispatch_async中來使用。多個block的運行是否併發或串行執行也依賴queue的是否併發或串行。

void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block); //這個函數能夠設置同步執行的block,它會等到在它加入隊列以前的block執行完畢後,纔開始執行。在它以後加入隊列的block,則等到這個block執行完畢後纔開始執行。

void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block); //同上,除了它是同步返回函數

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block); //延遲執行block

最後再來看看dispatch隊列的一個頗有特點的函數:

void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue);

它會把須要執行的任務對象指定到不一樣的隊列中去處理,這個任務對象能夠是dispatch隊列,也能夠是dispatch源。並且這個過程能夠是動態的,能夠實現隊列的動態調度管理等等。好比說有兩個隊列dispatchA和dispatchB,這時把dispatchA指派到dispatchB:

dispatch_set_target_queue(dispatchA, dispatchB);

那麼dispatchA上還未運行的block會在dispatchB上運行。這時若是暫停dispatchA運行:

dispatch_suspend(dispatchA);

則只會暫停dispatchA上原來的block的執行,dispatchB的block則不受影響。而若是暫停dispatchB的運行,則會暫停dispatchA的運行。

這裏只簡單舉個例子,說明dispatch隊列運行的靈活性,在實際應用中你會逐步發掘出它的潛力。

 

優勢:簡單方便,直接捕獲變量進入block(只讀訪問,除非用__block聲明)。

缺點:dispatch隊列暫時不支持cancel(取消),沒有實現dispatch_cancel()函數,不像NSOperationQueue。

 

ps: GCD所包含的方法並不限於任務隊列,還提供了 dispatch_semaphore_signal/dispatch_semaphore_wait 這樣的C語言接口(全部C語言代碼都能用OC編譯器編譯),並不會強制要求程序員必定要面向對象,避免了不少繁瑣步驟。

ps: !!! 重要的一點:block並非被主線程所執行,若是其它的線程在執行這個block是,調用了相似 _webView = [[UIWebView alloc] init]這樣的語句,那麼就會Crash:

Tried to obtain the web lock from a thread other than the main thread or the web thread. 
This may be a result of calling to UIKit from a secondary thread. Crashing now...

解決辦法就是,經過performSelectorOnMainThread或者GCD dispatch_的方式強制讓某些語句被主線程執行,或者經過信號量實現同步的方式來發送網絡請求,從而保證由主線程來調用全部涉及UI的語句。 

 

NSObject

全部繼承於NSObject類的類實例,均可以調用啓動多線程的方法。

如下3個函數是多線程的方法: 

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg,建立一個真正的新線程,不過沒法引用到這個線程

[self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES]; 通知主線程執行操做,通常用於更新界面

[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:YES];  等待selector指定的函數被某個線程執行完成後,當前線程再繼續當前任務

如下2個函數實際上不是多線程:

[self performSelector:@selector(run) withObject:nil];等待selector指定的函數被當前線程執行完成後,當前線程再繼續當前任務

performSelector:withObject:afterDelay:當前線程執行完成後,再啓動線程去執行selector所選擇的方法

 

Objective-C中調用函數的方法是「消息傳遞」,這個和普通的函數調用的區別是,你能夠隨時對一個對象傳遞任何消息,而不須要在編譯的時候聲明這些方法。因此Objective-C能夠在runtime的時候傳遞人和消息。

 

優勢:簡單方便,而且容許在運行時動態調用一個對象的任意方法

缺點:若是須要傳遞參數,不如GCD

 

NSThread

NSThread類是輕量級的多線程開發的類,使用起來也並不複雜,可是使用NSThread須要本身管理線程生命週期。NSThread中封裝了方法:
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument 建立一個真實的新線程,不過沒法引用到這個線程

- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 建立一個線程對象,能夠引用到一個新線程,調用start方法建立真實的線程。

[thread start]; 建立一個真實的線程,而且把線程對象thread做爲這個線程的引用

[thread cancel]; 取消對象thread所引用的那個線程

這個類命名空間下還提供了一些實用的類方法:

NSThread *current = [NSThread currentThread]; 獲取當前線程

NSThread *main = [NSThread mainThread]; 獲取主線程

[NSThread sleepForTimeInterval:2];  或  [NSThread sleepUntilDate:date];  暫停正在執行這段代碼的進程,也就是當前進程

 

優勢:以面對對象的觀點引用線程,方便線程生命週期的管理和多線程之間的同步;這個類能夠配合performselector方法使用;提供了一些實用的類方法,如獲取當前線程的引用、暫停當前線程等。

缺點:須要定義單獨的線程類來表明新的線程,較爲繁瑣,不夠直接

 

 

 NSOperation

 咱們先直接經過代碼來領會一個任務隊列。

invocation pperation(經過selector指定任務)

-(void)loadImageWithMultiThread{
    /*建立一個調用操做
     object:調用方法參數
    */
    NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
    //進程經過start方法後才能啓動操做,可是注意若是直接調用start方法,則此操做會在主線程中調用,通常不會這麼操做,而是添加到NSOperationQueue中
//    [invocationOperation start];
    
    //建立操做隊列
    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    //注意添加到操做隊後,隊列會開啓一個線程執行此操做
    [operationQueue addOperation:invocationOperation];
}

block operation(經過block指定任務)

NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
operationQueue.maxConcurrentOperationCount=5;//設置最大併發線程數
[operationQueue addOperationWithBlock:^{
            [self loadImage:[NSNumber numberWithInt:i]];
        }];
//更新UI界面,此處調用了主線程隊列的方法(mainQueue是UI主線程)
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
    [self updateImageWithData:data andIndex:i];
}];

概念上,一個任務隊列operation queue至關於一個由不少線程和任務組成的池。在一些應用場景之下,好比當須要下載不少張圖片,能夠把下載任務和相應的線程統一交給operation queue來管理,方便了操做。

一個NSOperation的實例定義了一個任務,能夠經過調用start方法在當前線程啓動它對應的任務,不過通常不這麼作,而是把operation加入Operation queue中統一管理。對於invocation operation和block operation,兩種方式本質沒有區別,可是是後者使用Block形式進行代碼組織,傳參容易,使用相對方便。

 

優勢:統一管理大量的任務和線程,適用於下載大量圖片等場景。

 

 

多線程的應用

一、操做數據模型

多個任務同時操做數據模型,線程安全須要仔細思考,對於數據模型對象的操做須要用@synchronized。

二、網絡請求

網絡請求是多線程的重要應用,下一篇博客會講。

相關文章
相關標籤/搜索