iOS開發(fā)多線程-NSOperation \ GCD詳解

iOS多線程開發(fā)必須知道的概念名詞:
1. 進程
  • 進程(process)就是一個正在執(zhí)行的程序的實例。也就是說我們的每一個APP程序在執(zhí)行時實例都是一個進程,也可以說在APP執(zhí)行時,它只擁有唯一的一個進程。
  • 每一個進程都是獨立的,每一個進程均在專屬的內(nèi)存空間內(nèi),iOS中每一個App(一個進程)都有自己獨特的內(nèi)存和磁盤空間,別的App(進程)是不允許訪問的(越獄除外)。iOS開發(fā)中應(yīng)用程序之間互相調(diào)用包括調(diào)用發(fā)短信、打電話等進程相關(guān)操作的API都被封裝到UIApplication這個類中了。
  • 每個進程至少擁有一個線程。
  • 在UNIX和Linux系統(tǒng)中是有進程層次結(jié)構(gòu)的,當進程創(chuàng)建了另一個進程后,父進程和子進程就以某種方式繼續(xù)保持聯(lián)系。子進程自身可以創(chuàng)建更多的進程,組成一個進程的層次結(jié)構(gòu)。(進程只有一個父進程但是可以有0個,1個或多個子進程。)而Windows中沒有進程層次的概念,所有進程地位相同。唯一類似進程層次的地方是在創(chuàng)建進程的時候父進程得到一個特別的令牌(稱為句柄),該令牌可以用來控制子進程。但是父進程可以把這個令牌傳送給其他進程,這樣就不存在進程層次了。
  • 進程有3種狀態(tài),分別是:
    1)運行態(tài)(該時刻進程實際占用CPU)。
    2)就緒態(tài)(可運行,但因為其他進程正在運行而暫時停止)。
    3)阻塞態(tài)(除非某種外部事件發(fā)生,否則進程不能運行)。
2. 線程
  • 進程用于把資源集中到一起,而線程是在CUP上被調(diào)度的實體。
  • 線程擁有自己的寄存器,用來保存當前的工作變量,稱作線程的上下文。還擁有一個自己的堆棧,用來記錄執(zhí)行歷史。
  • 線程之間共享同樣的內(nèi)存空間和全局變量,一個線程可以讀、寫或甚至清除另一個線程的堆棧。
  • 如果多個線程都是CPU密集型(也稱計算密集型)那并不能獲得性能上的增強,如果是I/O密集型則能加快程序執(zhí)行速度。
進程和線程的關(guān)系
  • 線程只是一個進程中的不同執(zhí)行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,它們共享進程的地址空間。一個線程crash就等于整個進程crash。
  • 進程的創(chuàng)建和操作開銷很大,某種意義上講線程就相當于輕量級的進程。多線程使用的其中一個理由就是因為線程比進程更輕量級,線程比進程更容易(即更快)的創(chuàng)建和撤銷。在許多系統(tǒng)中,創(chuàng)建一個線程比創(chuàng)建一個進程要快10~100倍。


    進程線程關(guān)系示意圖.jpeg

多線程(英語:multithreading),是指從軟件或者硬件上實現(xiàn)多個線程并發(fā)執(zhí)行的技術(shù)。

多線程好處多的同時也易引發(fā)一些問題,“數(shù)據(jù)競爭”-多個線程操作同一資源時可能會導致數(shù)據(jù)的不一致;“死鎖”-兩個或兩個以上的線程在執(zhí)行過程中,因爭奪資源而造成的一種互相等待的現(xiàn)象;使用太多的線程會消耗大量內(nèi)存,因為每個線程都有自己的寄存器。進程太多的時候也有消耗太大的問題。

3. 串行(serial) & 并發(fā)(concurrent)& 并行

串行是同步線程的實現(xiàn)方式,就是任務(wù)A執(zhí)行結(jié)束才能開始執(zhí)行B,單個線程只能執(zhí)行一個任務(wù)。
并發(fā)并行其實是異步線程實現(xiàn)的兩種形式。并行其實是真正的異步,多核CUP可以同時開啟多條線程供多個任務(wù)同時執(zhí)行,互不干擾。并發(fā)是偽異步,單個CUP一個時刻只能有一個線程執(zhí)行,想執(zhí)行多個任務(wù)就必須不斷切換執(zhí)行任務(wù)的線程。

4. 同步 & 異步

同步:多個任務(wù)情況下,一個任務(wù)A執(zhí)行結(jié)束,才可以執(zhí)行另一個任務(wù)B。只存在一個線程。
異步:多個任務(wù)情況下,一個任務(wù)A正在執(zhí)行,同時可以執(zhí)行另一個任務(wù)B。任務(wù)B不用等待任務(wù)A結(jié)束才執(zhí)行。存在多條線程。

5. 調(diào)度隊列(Dispatch Queue)

調(diào)度隊列是執(zhí)行處理的隊列也是GCD的基本概念,它按照執(zhí)行任務(wù)添加的順序(即FIFO-先進先出順序)執(zhí)行處理。調(diào)度隊列在執(zhí)行處理時存在兩種Dispatch Queue,一種是等待現(xiàn)在執(zhí)行中處理的Serial Dispatch Queue,另一種是不等待現(xiàn)在執(zhí)行中處理的Concurrent Dispatch Queue,稍后會對這兩種隊列詳細的介紹。官方的說法是有三種隊列,還要一種叫Main dispatch queueMain dispatch queue其實也可以歸為Serial Dispatch Queue,不過由于它是主線程隊列所以單拿了出來。

iOS多線程技術(shù)對比

  • pthread

pthread(POSIX thread)是一套通用的多線程API,適用于Unix、Linux、Windows等系統(tǒng),跨平臺、可移植的C語言框架,線程生命周期由開發(fā)者管理,使用難度大。GCD的底層實現(xiàn)庫中也有用到Libc(pthreads)

  • NSThread

NSThread是這幾種方法里面相對輕量級的,但需要管理線程的生命周期、同步、加鎖問題,這會導致一定的性能開銷,同時在多個線程開發(fā)時不便于開發(fā)維護。詳細使用可參考這篇博客

  • NSOperation

  • NSOperation是基于OC實現(xiàn)的,它以面向?qū)ο蟮姆绞椒庋b了需要執(zhí)行的操作,然后可以將這個操作放到一個NSOperationQueue中去異步執(zhí)行。它是線程安全的,開發(fā)者不必關(guān)心線程管理、同步等問題。
  • NSOperation類是一個抽象類來封裝一個任務(wù)相關(guān)的代碼和數(shù)據(jù),不能直接被使用。可以使用它的兩個子類
    NSInvocationOperationNSBlockoperation來執(zhí)行實際的任務(wù),當然你也可以自己封裝一個子類來實現(xiàn)(只需要重載-(void)main這個方法,在這個方法里面添加需要執(zhí)行的操作。)。
  • NSOperation可以取消添加的執(zhí)行任務(wù)。一個NSOperation對象是一個單次對象(single-shot object)只能執(zhí)行一次任務(wù),不能再次執(zhí)行它。
  • GCD

  • Grand Central Dispatch(GCD)是蘋果開發(fā)的基于XNU內(nèi)核級線程管理技術(shù),優(yōu)化對多核處理器的支持。

  • GCD是一個基于線程池的任務(wù)并行執(zhí)行模式。其基本思想是將線程池的管理從開發(fā)人員手中轉(zhuǎn)移出來,并更接近操作系統(tǒng)(更高效)。開發(fā)人員不用管理線程,只需要把任務(wù)添加到執(zhí)行隊列就可以。

  • GCD基于C語言實現(xiàn),不過使用了Block,因而API非常簡潔易用。

  • GCD需要開發(fā)者釋放自己創(chuàng)建的隊列Dispatch Queue,系統(tǒng)提供的標準隊列是全局的所以不用釋放。

  • NSOperation相對于GCD的優(yōu)勢:

1>任務(wù)可以添加依賴關(guān)系,即便是異步執(zhí)行也可以給部分任務(wù)執(zhí)行順序;
2>添加的任務(wù)如果不是已經(jīng)執(zhí)行,取消是比較方便的;
3>可以監(jiān)聽任務(wù)的狀態(tài)進而做其他的處理;
4>任務(wù)優(yōu)先級設(shè)置比較方便,GCD可以給隊列設(shè)置優(yōu)先級而且只有3種;
5>最大并發(fā)數(shù)設(shè)置;GCD實現(xiàn)比較復雜些(用信號量);
6>繼承NSOperation自定義。

NSOperation

NSOperation實現(xiàn)多線程主要步驟是:
1> 封裝執(zhí)行的操作到一個NSOperation對象中

2> 將封裝的NSOperation對象添加到NSOperationQueue

3> 系統(tǒng)會自動為NSOperation對象封裝的任務(wù)開啟一條線程執(zhí)行 或者 不加入隊列調(diào)用-(void)start:在主線程執(zhí)行

- (void)operationManage{
    // 這樣在主線程執(zhí)行其實是畫蛇添足的,只是為了做說明而寫
    NSBlockOperation *downloadImgPng = [NSBlockOperation blockOperationWithBlock:^{
       //downloadImage 任務(wù)
        NSLog(@"png -- 當前線程%@",[NSThread currentThread]);
    }];
    [downloadImgPng start];
}
//  NSInvocationOperation執(zhí)行方式
- (void)operationManage{
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage) object:nil];

    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    // 同一時間最多開啟的線程數(shù) 
    operationQueue.maxConcurrentOperationCount = 6;
    [operationQueue addOperation:invocationOperation];

    // 取消所有隊列中的任務(wù)
//    [operationQueue cancelAllOperations];
    
    // 取消執(zhí)行的任務(wù)
//    [invocationOperation cancel];
}

- (void)downloadImage{

}
//  NSBlockOperation執(zhí)行方式
- (void)operationManage{
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
       //downloadImage 任務(wù)
    }];
    
    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    // 同一時間最多開啟的線程數(shù)
    operationQueue.maxConcurrentOperationCount = 6;
    [operationQueue addOperation:blockOperation];
}
- (void)operationManage{
    /*
     * 多任務(wù)隊列添加依賴--串行執(zhí)行
     * 一般多任務(wù)隊列默認是并行執(zhí)行,添加依賴可按依賴條件順序執(zhí)行
     */
    NSBlockOperation *downloadImgPng = [NSBlockOperation blockOperationWithBlock:^{
       //downloadImage 任務(wù)
        NSLog(@"png -- 當前線程%@",[NSThread currentThread]);
    }];
    
    NSBlockOperation *downloadImgJpg = [NSBlockOperation blockOperationWithBlock:^{
        //downloadImage 任務(wù)
        NSLog(@"jpg -- 當前線程%@",[NSThread currentThread]);
    }];
    [downloadImgJpg addDependency:downloadImgPng];
    
    NSBlockOperation *downloadImgPdf = [NSBlockOperation blockOperationWithBlock:^{
        //downloadImage 任務(wù)
        NSLog(@"pdf -- 當前線程%@",[NSThread currentThread]);
    }];
    [downloadImgPdf addDependency:downloadImgJpg];
    
    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    // 同一時間最多開啟的線程數(shù)
    operationQueue.maxConcurrentOperationCount = 6;

    [operationQueue addOperations:@[downloadImgPng,downloadImgJpg,downloadImgPdf] waitUntilFinished:NO];
}

NSOperation對象執(zhí)行狀態(tài)監(jiān)聽

NSOperation對象執(zhí)行狀態(tài).png

/*
此屬性指定應(yīng)用于添加到隊列中的操作對象的服務(wù)級別。如果操作對象具有顯式的服務(wù)水平集,則使用該值。此屬性的默認值取決于您創(chuàng)建隊列的方式。自己創(chuàng)建的隊列,默認值是NSOperationQualityOfServiceBackground。為隊列的mainqueue方法返回,默認值是nsoperationqualityofserviceuserinteractive和不能改變的。

服務(wù)級別影響給定操作對象訪問系統(tǒng)資源的優(yōu)先級,如CPU時間、網(wǎng)絡(luò)資源、磁盤資源等。具有較高服務(wù)質(zhì)量級別的操作在系統(tǒng)資源上被賦予更大的優(yōu)先權(quán),以便它們能更快地執(zhí)行任務(wù)。您使用服務(wù)級別確保響應(yīng)顯式用戶請求的操作優(yōu)先于不重要的工作。
*/
@property NSQualityOfService qualityOfService;
typedef enum NSQualityOfService : NSInteger {
    NSQualityOfServiceUserInteractive = 0x21,
    NSQualityOfServiceUserInitiated = 0x19,
    NSQualityOfServiceUtility = 0x11,
    NSQualityOfServiceBackground = 0x09,
    NSQualityOfServiceDefault = -1
} NSQualityOfService;

NSQualityOfServiceUserInteractive 用于直接提供交互式UI的工作。例如,處理控件事件或繪制到屏幕上。
NSQualityOfServiceUserInitiated 用于執(zhí)行用戶明確要求的工作,必須立即提交結(jié)果,以便進一步進行用戶交互。例如,在用戶在郵件列表中選擇郵件后,加載電子郵件。
NSQualityOfServiceUtility 用于執(zhí)行用戶不太可能立即等待結(jié)果的工作。這項工作可能是用戶要求的,也可能是自動啟動的,并且經(jīng)常使用非模態(tài)進度指示器在用戶可見的時間尺度上運行。例如,周期性內(nèi)容更新或大容量文件操作,如媒體導入。
NSQualityOfServiceBackground用于非用戶發(fā)起或可見的工作。一般來說,用戶不知道這項工作甚至正在發(fā)生。例如,預取內(nèi)容,搜索索引、備份或同步與外部系統(tǒng)的數(shù)據(jù)。
NSQualityOfServiceDefault指示沒有明確的服務(wù)質(zhì)量信息。只要有可能,適當?shù)姆?wù)質(zhì)量由可用的來源決定。否則,選擇的可能是NSQualityOfServiceUserInteractiveNSQualityOfServiceUtility之間服務(wù)水平的任意一種。

/*
此屬性包含操作的相對優(yōu)先級。這個值是用來影響其中的操作和執(zhí)行順序出列。
官方建議:為了確定優(yōu)先級,應(yīng)該始終使用這些常量(而不是定義的值)。
*/
@property NSOperationQueuePriority queuePriority;
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

以上兩個屬性通過設(shè)置合適的值,能讓資源利用更加合理。其他的API可查看官方文檔使用,特別注意的是- (void)waitUntilFinished;這個接口要慎用,該接口絕不能在主線程調(diào)用,會產(chǎn)生死鎖卡死主線程。一般來講用- (void)addDependency:(NSOperation *)op;依賴API就夠了,簡單易用。

- (void)waitUntilFinished;接口的文檔解釋:

An operation object must never call this method on itself and should avoid calling it on any operations submitted to the same operation queue as itself. Doing so can cause the operation to deadlock. Instead, other parts of your app may call this method as needed to prevent other tasks from completing until the target operation object finishes. It is generally safe to call this method on an operation that is in a different operation queue, although it is still possible to create deadlocks if each operation waits on the other.
A typical use for this method would be to call it from the code that created the operation in the first place. After submitting the operation to a queue, you would call this method to wait until that operation finished executing.

翻譯:操作對象絕不能自己調(diào)用這個方法,應(yīng)該避免在提交給同一操作隊列的任何操作中調(diào)用它。這樣做可能導致操作死鎖。相反,應(yīng)用程序的其他部分可以根據(jù)需要調(diào)用此方法,以防止其他任務(wù)完成,直到目標操作對象完成為止。一般來說,在不同的操作隊列中調(diào)用這種方法是安全的,但如果每個操作都等待另一個操作,仍然有可能造成死鎖。
這種方法的一個典型用途是首先從創(chuàng)建操作的代碼調(diào)用它。在向隊列提交操作之后,您將調(diào)用此方法等待該操作完成執(zhí)行。

waitUntilFinished簡單使用.png

GCD的主要API使用

  • GCD的實現(xiàn)步驟

GCD實現(xiàn)多線程的步驟主要有2步:
1>創(chuàng)建隊列
2>添加執(zhí)行任務(wù)到隊列中
也可以是1步,添加執(zhí)行任務(wù)到系統(tǒng)標準隊列
沒錯,就是這么簡單易用!以下是代碼片段:

// 創(chuàng)建隊列 ,手動創(chuàng)建的隊列需要做釋放處理,因為Dispatch Queue并沒有被作為OC對象處理
/*
 * 1.創(chuàng)建串行隊列
 * 串行隊列有兩種創(chuàng)建方式
 */
    dispatch_queue_t serialQueue = dispatch_queue_create("com.example.gcd.mytest", NULL);
    // 釋放隊列
    dispatch_release(serialQueue);

    dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.mytest", DISPATCH_QUEUE_SERIAL);
    // 釋放隊列
    dispatch_release(queue);

// 2.創(chuàng)建并行隊列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.gcd.mytest", DISPATCH_QUEUE_CONCURRENT);
// 3.添加同步任務(wù)到隊列中
    dispatch_sync(concurrentQueue, ^{
        // 執(zhí)行任務(wù)
    });
    
// 4.添加異步任務(wù)到隊列中
    dispatch_async(concurrentQueue, ^{
       // 執(zhí)行任務(wù)
    });
    
// 5.釋放隊列( iOS 6.0 or Mac OS X 10.8 以上系統(tǒng)可以管理GCD對象無需手動釋放)
    dispatch_release(concurrentQueue);
// 一步搞定
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 執(zhí)行操作
    });

GCD的隊列主要有兩種Serial Dispatch QueueConcurrent Dispatch Queue。前者只有一個線程,后者根據(jù)任務(wù)量,可以開啟多條線程。當然多個Serial Dispatch Queue是可以并行執(zhí)行的。

兩種隊列和線程的關(guān)系.png
  • Main Dispatch Queue / Global Dispatch Queue

Main Dispatch QueueGlobal Dispatch Queue是系統(tǒng)提供的標準Dispatch Queue,這兩個標準隊列還有一個共同的優(yōu)點,那就是相比于手動創(chuàng)建的隊列,這兩個隊列不需要開發(fā)者做隊列釋放的操作,因為這兩個隊列對應(yīng)用程序而言是全局的,詳細可查看官方文檔

Main Dispatch Queue可能你已經(jīng)猜到了,沒錯,它就是主線程隊列,追加到Main Dispatch Queue的處理在主線程的RunLoop中執(zhí)行,一些需要更新UI界面的操作可以放到這個線程中執(zhí)行。

Global Dispatch QueueConcurrent Dispatch Queue類型的隊列。所以我們一般是不用逐個生成Concurrent Dispatch Queue隊列的,只要使用全局隊列Global Dispatch Queue就可以了。

    // 主隊列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    // 全局隊列
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

創(chuàng)建全局隊列的APIdispatch_get_global_queue(long identifier, unsigned long flags);需要傳兩個參數(shù),第一個是優(yōu)先級,根據(jù)實際處理內(nèi)容選擇合適的優(yōu)先級。第二個官方文檔稱是為將來使用預留的,一般傳數(shù)字0即可。
Global Dispatch Queue有4個執(zhí)行優(yōu)先級。

#define DISPATCH_QUEUE_PRIORITY_HIGH 2               // 高優(yōu)先級
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0            // 默認優(yōu)先級 
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)             // 低優(yōu)先級
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后臺優(yōu)先級
  • dispatch_sync / dispatch_asycn

dispatch_asycn是異步處理函數(shù),該函數(shù)不會等待任務(wù)執(zhí)行完,不會一直占用當前線程。

dispatch_sync是同步處理函數(shù),在該函數(shù)中執(zhí)行的任務(wù)不執(zhí)行完,該函數(shù)會一直在當前線程等待。也可以說這個函數(shù)是簡化版的dispatch_group_wait函數(shù)。

dispatch_sync要慎用,因為使用不當就會引起死鎖。
比如在主線程調(diào)用:

    dispatch_queue_t mianQueue = dispatch_get_main_queue();
    dispatch_async(mainQueue, ^{
        dispatch_sync(mainQueue, ^{
            NSLog(@"Hello World");
        });
    });

Serial Diapatch Queue中調(diào)用也是一樣

    dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.mytest", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        dispatch_sync(queue, ^{
            NSLog(@"Hello World");
        });
    });

死鎖的原因很明顯就是兩個操作在同一個線程里互相等待對方執(zhí)行完,一直互相等待,誰也不執(zhí)行。

  • dispatch_set_target_queue

手動創(chuàng)建的隊列可以通過dispatch_set_target_queue設(shè)定執(zhí)行的優(yōu)先級。將Dispatch Queue指定為dispatch_set_target_queue的函數(shù)參數(shù),不僅可以變更Dispatch Queue的執(zhí)行優(yōu)先級,還可以作為執(zhí)行階層。

比如,將一個普通的Serial Diapatch Queue設(shè)定為與“后臺優(yōu)先級全局隊列”一樣的優(yōu)先級和階層,那么在執(zhí)行時,它的執(zhí)行優(yōu)先級將高于其他普通的Serial Diapatch QueueConcunrrent Diapatch Queue,它的階層也要比普通的Serial Diapatch QueueConcunrrent Diapatch Queue高,當它與“后臺優(yōu)先級全局隊列”階層一樣時意味著,如果它在執(zhí)行,其他普通的Serial Diapatch QueueConcunrrent Diapatch Queue都不能和它并行執(zhí)行,必須等它執(zhí)行完才能執(zhí)行。

    // 創(chuàng)建串行隊列
    dispatch_queue_t serialQueue = dispatch_queue_create("com.example.gcd.mytest", NULL);
    
    // 創(chuàng)建并行隊列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.gcd.mytest", DISPATCH_QUEUE_CONCURRENT);
    
    /*
     * 參照serialQueue的優(yōu)先級設(shè)置目標隊列 即concurrentQueue的優(yōu)先級
     * 第一個參數(shù)為要設(shè)置優(yōu)先級的queue,第二個參數(shù)是參照物,既將第一個queue的優(yōu)先級和第二個queue的優(yōu)先級設(shè)置一樣。
     */ 
    dispatch_set_target_queue(concurrentQueue, serialQueue);
  • dispatch_after

dispatch_after用于延時處理,需要注意的是并不是dispatch_after在延時指定時間后執(zhí)行,而是在指定時間把任務(wù)添加到隊列中,相當于加了一個計時器,時間到了就把任務(wù)添加到隊列中了。

栗子:
延時3秒打印Hello Word

    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull*NSEC_PER_SEC);
    dispatch_after(time, dispatch_get_main_queue(), ^{NSLog(@"Hello World");});

dispatch_time的第一個參數(shù)是起始時間可傳:DISPATCH_TIME_NOWDISPATCH_TIME_FOREVER
DISPATCH_TIME_NOW : 表示從現(xiàn)在開始 DISPATCH_TIME_FOREVER:表示持續(xù)等待,稍后會用到。
第二個參數(shù)是持續(xù)多久,"ull"是C語言的數(shù)值字面量,是顯示表明類型時使用的字符串(表示"unsight long long")為了精確時間寫上"ull"NSEC_PER_SEC是秒時間單位的一種,表示納秒級精確的一秒。

NSEC:納秒。
USEC:微妙。
SEC:秒
PER:每
#define NSEC_PER_SEC 1000000000ull  // 每秒有多少納秒
#define NSEC_PER_MSEC 1000000ull    // 每毫秒有多少納秒
#define USEC_PER_SEC 1000000ull     // 每秒有多少毫秒
#define NSEC_PER_USEC 1000ull       // 每毫秒有多少納秒
  • Dispatch Group

有時我們想等添加到隊列中的所有任務(wù)都執(zhí)行完再執(zhí)行結(jié)束處理。Serial Dispatch Queue好說,本來就是串行的。但是Concurrent Dispatch Queue就不行了,異步的我們根本不知道哪個是最后執(zhí)行完的。這個時候就可以用到Dispatch Group了。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{NSLog(@"block 0");});
    dispatch_group_async(group, queue, ^{NSLog(@"block 1");});
    dispatch_group_async(group, queue, ^{NSLog(@"block 2");});
    /*
     * 無論向什么樣的Dispatch Queue中添加任務(wù),使用Dispatch Group都可以監(jiān)聽這個任務(wù)的執(zhí)行結(jié)束
     * 所有任務(wù)結(jié)束時,會執(zhí)行dispatch_group_notify函數(shù)的block
     */ 
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"done");});
   
  // iOS 6.0 or Mac OS X 10.8 以上系統(tǒng)可以管理GCD對象無需手動釋放
    dispatch_release(group);

dispatch_group_wait也可以達到同樣的效果,不過前者更簡潔,所以建議用dispatch_group_notify,下邊是dispatch_group_wait的實現(xiàn)。

   dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{NSLog(@"block 0");});
    dispatch_group_async(group, queue, ^{NSLog(@"block 1");});
    dispatch_group_async(group, queue, ^{NSLog(@"block 2");});
    
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_FOREVER, 1ull*NSEC_PER_SEC);
    long result = dispatch_group_wait(group, time);
    /*
     指定DISPATCH_TIME_NOW,則不用任何等待,即可判斷Dispatch Group中的處理是否執(zhí)行結(jié)束
(在主線程的Runloop的每次循環(huán)中可檢查執(zhí)行是否結(jié)束,從而不耗費多余的等待時間)
     */
   // long result = dispatch_group_wait(group, DISPATCH_TIME_NOW);
    
    if (result == 0) {
        // group的全部任務(wù)執(zhí)行完畢
    }
    else {
        // group的某個任務(wù)還在執(zhí)行中
    }
    
    // iOS 6.0 or Mac OS X 10.8 以上系統(tǒng)可以管理GCD對象無需手動釋放
    // dispatch_release(group);
  • Dispatch Semaphore 信號量

GCD信號量的用法主要有兩種,一種是監(jiān)聽1個異步執(zhí)行結(jié)果,另外一種是控制線程并發(fā)數(shù)。
信號量的函數(shù)有3個:

  1. 創(chuàng)建信號量,傳入的value要大于等于0
    dispatch_semaphore_create(long value);
  2. 增加一個信號量
    dispatch_semaphore_signal(dispatch_semaphore_t dsema);
  3. 減少1個信號量,并且信號量為0則等待,等待時間取決于timeout時間參數(shù)
    dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

接下來看代碼:

  • 信號量監(jiān)聽1個異步執(zhí)行結(jié)果
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);// 信號量為0
    
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"run task 1");
        sleep(1);
        NSLog(@"complete task 1");
        // 信號量+1
        dispatch_semaphore_signal(semaphore);
    });
    
    // 若信號量為0則一直等待
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"繼續(xù)執(zhí)行函數(shù)");
  • 打印結(jié)果
2017-12-19 11:35:31.731268+0800 多線程[3482:317136] run task 1
2017-12-19 11:35:32.736671+0800 多線程[3482:317136] complete task 1
2017-12-19 11:35:32.737201+0800 多線程[3482:316871] 繼續(xù)執(zhí)行函數(shù)
  • 信號量控制線程數(shù)
// 信號量為2,相當于線程最大并發(fā)數(shù)為2
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 若信號量為0則一直等待,不為0則信號量-1并繼續(xù)執(zhí)行函數(shù)
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"run task 1");
        sleep(1);
        NSLog(@"complete task 1");
        dispatch_semaphore_signal(semaphore); // 信號量+1
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"run task 2");
        sleep(1);
        NSLog(@"complete task 2");
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"run task 3");
        sleep(1);
        NSLog(@"complete task 3");
        dispatch_semaphore_signal(semaphore);
    });
  • 打印結(jié)果
/*
1.信號量初始值為2(即線程最大并發(fā)數(shù)為2);
2.任務(wù)1執(zhí)行信號量-1,任務(wù)2執(zhí)行信號量-1,此時信號量為0(即兩個線程已經(jīng)有任務(wù)在執(zhí)行,任務(wù)3只能等待);
3.當任務(wù)1或任務(wù)2執(zhí)行完,則信號量+1,信號量不為0(即有一個可用線程,此時任務(wù)3可執(zhí)行)。
*/
2017-12-19 11:41:28.631848+0800 多線程[3542:324794] run task 2
2017-12-19 11:41:28.631849+0800 多線程[3542:324796] run task 1
2017-12-19 11:41:29.636731+0800 多線程[3542:324794] complete task 2
2017-12-19 11:41:29.636758+0800 多線程[3542:324796] complete task 1
2017-12-19 11:41:29.637094+0800 多線程[3542:324795] run task 3
2017-12-19 11:41:30.637980+0800 多線程[3542:324795] complete task 3

關(guān)于信號量和Dispatch Group異步線程的延伸閱讀

  • dispatch_once

dispatch_once大家比較熟,因為線程安全的單例模式常用到它。

+(instancetype)sharedSingleton{
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

還可以用@synchronized()這個互斥鎖實現(xiàn)單例模式:

+(instancetype)sharedSingleton{
    static id instance = nil;
    @synchronized (self) {
        if (!instance) {
            instance = [[self alloc] init];
        }
    }
    return instance;
}
  • dispatch_barrier_async

在訪問數(shù)據(jù)庫或文件時,為避免數(shù)據(jù)競爭,前面講到可以使用Serial Dispatch Queue。但是其實如果是讀取和讀取并行執(zhí)行是不會引起數(shù)據(jù)競爭的,如果能把這部分操作拆分出來,那無疑會提高訪問效率。dispatch_barrier_async配合手動創(chuàng)建的Concurrent Dispatch Queue就可以幫我們做到。在執(zhí)行dispatch_barrier_async時,它會等正在執(zhí)行的任務(wù)執(zhí)行完開始,當它結(jié)束后其他任務(wù)才會再開始執(zhí)行。在SDWebImage框架中也有用到它和它的同步函數(shù)dispatch_barrier_sync

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.mytest", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        // 執(zhí)行讀取操作
    });
    dispatch_async(queue, ^{
        // 執(zhí)行讀取操作
    });
    dispatch_barrier_async(queue, ^{
       // 執(zhí)行寫入操作
    });
    dispatch_async(queue, ^{
        // 執(zhí)行讀取操作
    });
    dispatch_async(queue, ^{
        // 執(zhí)行讀取操作
    });
  • dispatch_suspend / dospatch_resume

在隊列大量任務(wù)執(zhí)行時,需要臨時掛起隊列時調(diào)用dispatch_suspend
dispatch_suspend(queue)掛起隊列
dospatch_resume (queue)恢復隊列

GCD還提供可多線程讀取同一個大型文件的APIdispatch I/Odispatch Data,還有其他很多有意思有用的API,大家盡可以去查看官方文檔學習和使用。

參考

  • 蘋果官方文檔
  • 《現(xiàn)代操作系統(tǒng)》
  • 《Objective-C高級編程- iOS與OS X多線程和內(nèi)存管理》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內(nèi)容