iOS之多線程:線程的生命周期,NSThread、GCD、NSOperation的使用與總結(jié)

前言:

我負責努力,其余交給運氣。

正文:

閑暇之余,把線程的問題整理一下,感覺可能會有點長,所以先自分一下章節(jié),我將會按照以下幾個小節(jié)來展開描述:

1.多線程的基本概念

2.線程的狀態(tài)與生命周期

3.多線程的對比:pthread、NSThread、GCD、NSOperation

4.NSThread的使用

5.GCD的理解與使用

6.NSOperation的理解與使用

一、多線程的基本概念
  • 進程:是計算機中的程序關(guān)于某數(shù)據(jù)集合上的一次運行活動,是系統(tǒng)進行資源分配和調(diào)度的基本單位,是操作系統(tǒng)結(jié)構(gòu)的基礎(chǔ)。在當代面向線程設(shè)計的計算機結(jié)構(gòu)中,進程是線程的容器。簡單來說,進程可以看成App執(zhí)行的一個實例,一個進程可以擁有多個線程。線程與進程的一個主要區(qū)別是,同一進程內(nèi)的多個線程會共享部分狀態(tài),多個線程可以讀寫同一塊內(nèi)存(一個進程無法直接訪問另一進程的內(nèi)存)。

  • 線程:是操作系統(tǒng)能夠進行運算調(diào)度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中最少有一個線程,也可以有多個線程,每條線程可執(zhí)行不同的任務(wù)。

  • 主線程:當一個程序啟動時,就有一個進程被操作系統(tǒng)創(chuàng)建,與此同時一個線程也立刻運行,該線程通常叫做程序的主線程,因為它是程序開始時就執(zhí)行的,如果你需要再創(chuàng)建線程,那么創(chuàng)建的線程就是這個主線程的子線程。主線程的重要性體現(xiàn)在兩方面:1.是產(chǎn)生其他子線程的線程;2.通常它必須最后完成執(zhí)行比如執(zhí)行各種關(guān)閉動作。

  • 多線程:在同一時刻,一個CPU只能處理1條線程,但CPU可以在多條線程之間快速的切換,只要切換的足夠快,就造成了多線程一同執(zhí)行的假象。而我們運用多線程的目的在于,避免阻塞主線程;多線程是通過提高資源使用率來提高系統(tǒng)總體的效率。

  • 最后,做個形象的比喻:一個運行的App我們可以看做是奔跑的火車(一個進程),而一個火車最少要有一個火車頭(主線程,負責程序的運行,且一個進程至少包含一個線程),可以有多節(jié)車廂(多線程,每一節(jié)車廂可以看做是子線程,可執(zhí)行不同的任務(wù))。


二、線程的狀態(tài)與生命周期

如下圖所示,線程的生命周期是:新建、就緒、運行、阻塞、死亡:

線程狀態(tài)圖
  • 新建:實例化線程對象

  • 就緒:向線程對象發(fā)送start消息,線程對象被加入可調(diào)度線程池等待CPU調(diào)度。

  • 運行:CPU 負責調(diào)度可調(diào)度線程池中線程的執(zhí)行。線程執(zhí)行完成之前,狀態(tài)可能會在就緒和運行之間來回切換。就緒和運行之間的狀態(tài)變化由CPU負責,程序員不能干預(yù)。

  • 阻塞:當滿足某個預(yù)定條件時,可以使用休眠或鎖,阻塞線程執(zhí)行。sleepForTimeInterval(休眠指定時長),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥鎖等各種鎖)。

  • 死亡:正常死亡,線程執(zhí)行完畢。非正常死亡,當滿足某個條件后,在線程內(nèi)部中止執(zhí)行/在主線程中止線程對象(例如:exit和cancel)

  • [NSThread exit]:一旦強行終止線程,后續(xù)的所有代碼都不會被執(zhí)行。

  • [thread cancel]取消:并不會直接取消線程,只是給線程對象添加 isCancelled 標記。


三、多線程的對比:pthread、NSThread、GCD、NSOperation
線程對比圖

相比于pthread和NSThread,GCD和NSOperation更常用一些,所以著重說一下GCD和NSOperation:

GCD 和 NSOperation的區(qū)別主要表現(xiàn)在以下幾方面:

  1. GCD是一套 C 語言API,執(zhí)行和操作簡單高效,因此NSOperation底層也通過GCD實現(xiàn),這是他們之間最本質(zhì)的區(qū)別.因此如果希望自定義任務(wù),建議使用NSOperation;

  2. 依賴關(guān)系,NSOperation可以設(shè)置操作之間的依賴(可以跨隊列設(shè)置),GCD無法設(shè)置依賴關(guān)系,不過可以通過同步來實現(xiàn)這種效果;

  3. KVO(鍵值對觀察),NSOperation容易判斷操作當前的狀態(tài)(是否執(zhí)行,是否取消等),對此GCD無法通過KVO進行判斷;

  4. 優(yōu)先級,NSOperation可以設(shè)置自身的優(yōu)先級,但是優(yōu)先級高的不一定先執(zhí)行,GCD只能設(shè)置隊列的優(yōu)先級,如果要區(qū)分block任務(wù)的優(yōu)先級,需要很復(fù)雜的代碼才能實現(xiàn);

  5. 繼承,NSOperation是一個抽象類.實際開發(fā)中常用的是它的兩個子類:NSInvocationOperation和NSBlockOperation,同樣我們可以自定義NSOperation,GCD執(zhí)行任務(wù)可以自由組裝,沒有繼承那么高的代碼復(fù)用度;

  6. 效率,直接使用GCD效率確實會更高效,NSOperation會多一點開銷,但是通過NSOperation可以獲得依賴,優(yōu)先級,繼承,鍵值對觀察這些優(yōu)勢,相對于多的那么一點開銷確實很劃算,魚和熊掌不可得兼,取舍在于開發(fā)者自己;

  7. 可以隨時取消準備執(zhí)行的任務(wù)(已經(jīng)在執(zhí)行的不能取消),GCD沒法停止已經(jīng)加入queue 的 block(雖然也能實現(xiàn),但是需要很復(fù)雜的代碼)

基于GCD簡單高效,更強的執(zhí)行能力,操作不太復(fù)雜的時候,優(yōu)先選用GCD;而比較復(fù)雜的任務(wù)可以自己通過NSOperation實現(xiàn).


四、NSThread的使用
4.1 NSThread的創(chuàng)建

NSThread有三種創(chuàng)建方式:

  • init 創(chuàng)建好之后需要start啟動
  • detachNewThreadSelector 創(chuàng)建好之后自動啟動
  • performSelectorInBackground 創(chuàng)建好之后直接啟動

直接上代碼:

- (void)showNSThread{
    /** 方法一,需要start */
    NSThread* thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething1:) object:@"1"];
    [thread1 start];
     /** 方法二,創(chuàng)建好之后自動啟動 */
    [NSThread detachNewThreadSelector:@selector(doSomething2:) toTarget:self withObject:@"2"];
    /** 方法三,隱式創(chuàng)建,直接啟動 */
    [self performSelectorInBackground:@selector(doSomething3:) withObject:@"3"];
}
- (void)doSomething1:(NSObject*)obj{
    NSLog(@"doSometing1 ---- obj = %@",obj);
}
- (void)doSomething2:(NSObject*)obj{
    NSLog(@"doSometing2 ---- obj = %@",obj);
}
- (void)doSomething3:(NSObject*)obj{
    NSLog(@"doSometing3 ---- obj = %@",obj);
}
4.2 NSThread的一些屬性、實例方法、類方法**
  • 屬性:
/**
每個線程都維護了一個鍵-值的字典,它可以在線程里面的任何地方被訪問。
可以使用該字典來保存一些信息,這些信息在整個線程的執(zhí)行過程中都保持不變。
比如,你可以使用它來存儲在你的整個線程過程中 Run loop 里面多次迭代的狀態(tài)信息。
NSThread實例可以使用一下方法:
NSMutableDictionary *dict = [thread threadDictionary]; 
*/
@property (readonly, retain) NSMutableDictionary *threadDictionary;

@property double threadPriority ; //優(yōu)先級

/**NSQualityOfService說明:
NSQualityOfServiceUserInteractive:最高優(yōu)先級,主要用于提供交互UI的操作,比如處理點擊事件,繪制圖像到屏幕上
NSQualityOfServiceUserInitiated:次高優(yōu)先級,主要用于執(zhí)行需要立即返回的任務(wù)
NSQualityOfServiceDefault:默認優(yōu)先級,當沒有設(shè)置優(yōu)先級的時候,線程默認優(yōu)先級
NSQualityOfServiceUtility:普通優(yōu)先級,主要用于不需要立即返回的任務(wù)
NSQualityOfServiceBackground:后臺優(yōu)先級,用于完全不緊急的任務(wù)
*/
@property NSQualityOfService qualityOfService; //線程優(yōu)先級

@property (nullable, copy) NSString *name;//線程名稱

@property NSUInteger stackSize ;//線程使用棧區(qū)大小,默認是512K

@property (readonly, getter=isExecuting) BOOL executing;//線程是否正在執(zhí)行

@property (readonly, getter=isFinished) BOOL finished;//線程是否執(zhí)行結(jié)束

@property (readonly, getter=isCancelled) BOOL cancelled;//線程是否可被取消

  • 實例方法:
-(void)start; //啟動線程(init創(chuàng)建的線程,需start啟動,不自行運行)

-(BOOL)isMainThread; //是否為主線程

-(void)setName:(NSString *)n; //設(shè)置線程名稱

-(void)cancel ; //取消線程

-(void)main ; //線程的入口函數(shù)

-(void)isExecuting; //判斷線程是否正在執(zhí)行

-(void)isFinished; //判斷線程是否已經(jīng)結(jié)束

-(void)isCancelled; //判斷線程是否撤銷
  • 類方法:
+(void)currentThread; //獲取當前線程

+(BOOL)isMultiThreaded; //當前代碼運行所在線程是否是子線程

+(void)sleepUntilDate:(NSDate *)date; //當前代碼所在線程睡到指定時間

+(void)sleepForTimeInterval:(NSTimeInterval)ti; //當前線程睡多長時間

+(void)exit; //退出當前線程

+(double)threadPriority; //設(shè)置當前線程優(yōu)先級
/**給當前線程設(shè)定優(yōu)先級,調(diào)度優(yōu)先級的取值范圍是0.0 ~ 1.0,
默認0.5,值越大,優(yōu)先級越高。*/
+(BOOL)setThreadPriority:(double)p; 
/**
線程的調(diào)用都會有函數(shù)的調(diào)用,函數(shù)的調(diào)用就會有棧返回地址的記錄,
在這里返回的是函 數(shù)調(diào)用返回的虛擬地址,
說白了就是在該線程中函數(shù)調(diào)用的虛擬地址的數(shù)組
*/
+(NSArray *)callStackReturnAddresses;

+(NSArray *)callStackSymbols; //同上面的方法一樣,只不過返回的是該線程調(diào)用函數(shù)的名字數(shù)字

注:callStackReturnAddress和callStackSymbols這兩個函數(shù)可以同NSLog聯(lián)合使用來跟蹤線程的函數(shù)調(diào)用情況,是編程調(diào)試的重要手段。


五、GCD的理解與使用

5.1 GCD的基本概念

  • 任務(wù)(block):任務(wù)就是將要在線程中執(zhí)行的代碼,將這段代碼用block封裝好,然后將這個任務(wù)添加到指定的執(zhí)行方式(同步執(zhí)行和異步執(zhí)行),等待CPU從隊列中取出任務(wù)放到對應(yīng)的線程中執(zhí)行。

  • 同步(sync):一個接著一個,前一個沒有執(zhí)行完,后面不能執(zhí)行,不開線程。

  • 異步(async):開啟多個新線程,任務(wù)同一時間可以一起執(zhí)行。異步是多線程的代名詞。

  • 隊列:裝載線程任務(wù)的隊形結(jié)構(gòu)。(系統(tǒng)以先進先出的方式調(diào)度隊列中的任務(wù)執(zhí)行)。在GCD中有兩種隊列:串行隊列和并發(fā)隊列。

  • 并發(fā)隊列:線程可以同時一起進行執(zhí)行。實際上是CPU在多條線程之間快速的切換。(并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效)

  • 串行隊列:線程只能依次有序的執(zhí)行。

  • GCD總結(jié):添加任務(wù)(線程中執(zhí)行的操作block)且指定執(zhí)行任務(wù)的方式(異步dispatch_async,同步dispatch_sync),并將線程添加進隊列(自己創(chuàng)建或使用全局并發(fā)隊列)。

5.2 隊列的創(chuàng)建方式
使用dispatch_queue_create來創(chuàng)建隊列對象,傳入兩個參數(shù),第一個參數(shù)表示隊列的唯一標識符,可為空。第二個參數(shù)用來表示串行隊列(DISPATCH_QUEUE_SERIAL或零)或并發(fā)隊列(DISPATCH_QUEUE_CONCURRENT)。

//串行隊列
dispatch_queue_t queue1 = dispatch_queue_create("queue1",DISPATCH_QUEUE_SERIAL);

//并行隊列
dispatch_queue_t queue2 = dispatch_queue_create("queue2",DISPATCH_QUEUE_CONCURRENT);

GCD的隊列還有另外兩種:

  • 主隊列:dispatch_get_main_queue(),主隊列負責在主線程上調(diào)度任務(wù),如果在主線程上已經(jīng)有任務(wù)正在執(zhí)行,主隊列會等到主線程空閑后再調(diào)度任務(wù)。通常是返回主線程更新UI的時候使用。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
      // 耗時操作放在這里
      dispatch_async(dispatch_get_main_queue(), ^{
     // 回到主線程進行UI操作
      });
});
  • 全局并發(fā)隊列:dispatch_get_global_queue(0, 0)全局并發(fā)隊列是就是一個并發(fā)隊列,是為了讓我們更方便的使用多線程。
//全局并發(fā)隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//全局并發(fā)隊列的優(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)先級

//iOS8開始使用服務(wù)質(zhì)量,現(xiàn)在獲取全局并發(fā)隊列時,可以直接傳0
dispatch_get_global_queue(0, 0);

5.3 同步、異步&任務(wù)

  • 同步(sync):使用dispatch_sync來表示
  • 異步(async):使用dispatch_async來表示。
  • 任務(wù):就是將要在線程中執(zhí)行的代碼,將這段代碼用block封裝好。
    // 同步執(zhí)行任務(wù)
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        // 任務(wù)放在這個block里
        NSLog(@"我是同步執(zhí)行的任務(wù)");
    });
    // 異步執(zhí)行任務(wù)
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 任務(wù)放在這個block里
        NSLog(@"我是異步執(zhí)行的任務(wù)");
    });

5.4 GCD的使用
由于有多種隊列(串行、并發(fā)、主隊列)和兩種執(zhí)行方式(同步、異步),所以他們之間有多種組合方式:串行同步、串行異步、并發(fā)同步、并發(fā)異步、主隊列同步、主隊列異步。

  • 5.4.1 串行同步:執(zhí)行完一個任務(wù),再執(zhí)行下一個任務(wù)。不開啟新線程。
/** 串行同步 */
- (void)serialSynchronization{
    // 串行隊列
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
    // 同步執(zhí)行
    dispatch_sync(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"串行同步1,%@",[NSThreadcurrentThread]);
        }
    });
    dispatch_sync(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"串行同步2,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)1");
    dispatch_sync(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"串行同步3,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)2");
}

執(zhí)行結(jié)果為順序執(zhí)行,且沒有開辟新線程,都在主線程,阻塞了主線程,如圖:

串行同步執(zhí)行結(jié)果
  • 5.4.2 串行異步:開啟新線程,但因為任務(wù)是串行的,所以還是按順序執(zhí)行任務(wù)。
 /** 串行異步 */
- (void)serialAsynchronous {
    // 串行隊列
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
    // 異步執(zhí)行
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"串行異步1,%@",[NSThreadcurrentThread]);
        }
    });
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"串行異步2,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)1");
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"串行異步3,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)2");
}

執(zhí)行結(jié)果如下,因為是串行隊列,所以異步線程按照順序執(zhí)行;但是異步開辟了新的子線程,所以沒有阻塞主線程:

串行異步執(zhí)行結(jié)果
  • 5.4.3 并發(fā)同步
/** 并發(fā)同步 */ 
- (void)concurrentSynchronization {
    // 并發(fā)隊列
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    // 同步執(zhí)行
    dispatch_sync(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"并發(fā)同步1,%@",[NSThreadcurrentThread]);
        }
    });
    dispatch_sync(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"并發(fā)同步2,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)1");
    dispatch_sync(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"并發(fā)同步3,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)2");
}

執(zhí)行結(jié)果如下,雖然我們選擇的并發(fā)隊列,但是因為是同步線程,所以任務(wù)依然是順序執(zhí)行,沒有開辟新線程,依然在主線程上執(zhí)行,且阻塞了主線程:

并發(fā)同步執(zhí)行結(jié)果
  • 5.4.4 并發(fā)異步
/** 并發(fā)異步 */
- (void)concurrentAsynchronization {
    // 并發(fā)隊列
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    // 異步執(zhí)行
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"并發(fā)異步1,%@",[NSThreadcurrentThread]);
        }
    });
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"并發(fā)異步2,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)1");
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"并發(fā)異步3,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)2");
}

執(zhí)行結(jié)果如下,執(zhí)行順序無序(并發(fā))、開辟了多條子線程、沒有阻塞主線程:

并發(fā)異步執(zhí)行結(jié)果
  • 5.4.5 主隊列同步
/** 主隊列同步 */
- (void)syncMain {
    // 主隊列
    NSLog(@"主線程執(zhí)行函數(shù)1");
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"主隊列同步1,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)2");
    dispatch_sync(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"主隊列同步2,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)3");
    dispatch_sync(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"主隊列同步3,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)4");
}

執(zhí)行結(jié)果如下,xcode8之后,不會阻塞,會崩潰。不過話說回來,主隊列同步造成死鎖,我們都知道是因為相互等待,可是阻塞、等待的是誰呢?這里要說的是:阻塞的是隊列, 不是線程,我們可能會疑惑,為什么其他串行同步不會死鎖:輸出block確實也在主線程執(zhí)行, 如果sync阻塞的是主線程, 那按理這樣確實會死鎖, 但實際上sync阻塞的是主隊列, 而不是主線程。其他串行同步運行結(jié)果是, dispatch_sync運行在主隊列, 而其block運行在queue隊列, 雖然兩個隊列都是由主線程執(zhí)行, 由于dispatch_sync阻塞的只是主隊列, block在queue隊列由主線程執(zhí)行完并返回, 從而dispatch_sync返回, 之后主隊列結(jié)束阻塞繼續(xù)執(zhí)行下面任務(wù). 此處queue隊列是運行在主線程的另一串行隊列(是不是很懵~~ 哈哈~~ 看下面模擬阻塞代碼會清楚一些)。

我們自己模擬出主隊列阻塞可能更容易理解:

dispatch_queue_t queue = dispatch_queue_create("queue1", DISPATCH_QUEUE_SERIAL);//可以把queue理解為主隊列
dispatch_sync(queue, ^{//可以把這理解為主線程
    dispatch_sync(queue, ^{//主線程中主隊列同步   
    });
});

這樣就把串行隊列queue阻塞了.

主隊列同步執(zhí)行結(jié)果
  • 5.4.6 主隊列異步
/** 主隊列異步 */
- (void)asyncMain {
    // 主隊列
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"主隊列異步1,%@",[NSThreadcurrentThread]);
        }
    });
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"主隊列異步2,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)1");
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"主隊列異步3,%@",[NSThreadcurrentThread]);
        }
    });
    NSLog(@"主線程執(zhí)行函數(shù)2");
}

執(zhí)行結(jié)果如下,在主線程中任務(wù)按順序執(zhí)行,但并沒有阻塞主線程:

主隊列異步

我們通過上面六種組合,可以看到,同步不論串行還是并發(fā),都沒有開辟新線程,運行在主線程上,且都是順序執(zhí)行,所以并沒有并發(fā)效果。異步不論串行還是并發(fā),都開啟了新線程(子線程),但是可見的是,多條異步串行,只開辟了一個子線程,多條異步并發(fā),開啟了多條子線程。主隊列異步,并沒有開啟子線程,且為有序串行執(zhí)行,但并沒有阻塞主線程。

  • 5.4.7 GCD線程間的通訊
    開發(fā)中需要在主線程上進行UI的相關(guān)操作,通常會把一些耗時的操作放在其他線程,比如說圖片文件下載等耗時操作。當完成了耗時操作之后,需要回到主線程進行UI的處理,這里就用到了線程之間的通訊。
- (IBAction)communicationBetweenThread:(id)sender {
    // 異步
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 耗時操作放在這里,例如下載圖片。(運用線程休眠兩秒來模擬耗時操作)
        [NSThread sleepForTimeInterval:2];
        NSString *picURLStr = @"https://123.jpg";
        NSURL *picURL = [NSURL URLWithString:picURLStr];
        NSData *picData = [NSData dataWithContentsOfURL:picURL];
        UIImage *image = [UIImage imageWithData:picData];
        // 回到主線程處理UI
        dispatch_async(dispatch_get_main_queue(), ^{
            // 在主線程上添加圖片
            self.imageView.image = image;
        });
    });
}
  • 5.4.8 GCD柵欄
    當任務(wù)需要異步進行,但是這些任務(wù)需要分成兩組來執(zhí)行,第一組完成之后才能進行第二組的操作。這時候就用了到GCD的柵欄方法dispatch_barrier_async和dispatch_barrier_sync詳情可查閱官方文檔。他們的不同之處在于:

dispatch_barrier_sync 將自己的任務(wù)插入到隊列的時候,需要等待自己的任務(wù)結(jié)束之后才會繼續(xù)插入被寫在它后面的任務(wù),然后執(zhí)行它們。

dispatch_barrier_async 將自己的任務(wù)插入到隊列之后,不會等待自己的任務(wù)結(jié)束,它會繼續(xù)把后面的任務(wù)插入到隊列,然后等待自己的任務(wù)結(jié)束后才執(zhí)行后面任務(wù)。

實際上類似于同步異步,下面以dispatch_barrier_async為例:

- (void)barrierGCD {
    // 并發(fā)隊列
    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    // 異步執(zhí)行
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"柵欄:并發(fā)異步1,%@",[NSThreadcurrentThread]);
        }
    });
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"柵欄:并發(fā)異步2,%@",[NSThreadcurrentThread]);
        }
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"------------barrier------------%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"柵欄:并發(fā)異步3,%@",[NSThreadcurrentThread]);
        }
    });
    dispatch_async(queue, ^{
        for(inti =0; i <3; i++) {
            NSLog(@"柵欄:并發(fā)異步4,%@",[NSThreadcurrentThread]);
        }
    });
}

執(zhí)行結(jié)果如下,開啟了多條線程,所有任務(wù)都是并發(fā)異步進行。但是第一組完成之后,才會進行第二組的操作

柵欄執(zhí)行結(jié)果

需要注意的是:
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block),柵欄函數(shù)中傳入的參數(shù)隊列必須是由 dispatch_queue_create 方法創(chuàng)建的隊列,否則,與dispatch_async無異,起不到“柵欄”的作用了,對于dispatch_barrier_sync也是同理。

  • 5.4.9 GCD隊列組
    平時在進行多線程處理任務(wù)時,有時候希望多個任務(wù)之間存在著一種聯(lián)系,希望在所有的任務(wù)執(zhí)行完后做一些總結(jié)性處理。那么就可以將多個任務(wù)放在一個任務(wù)組中進行統(tǒng)一管理。dispatch提供了相應(yīng)的API供我們完成這一需求。

(1)dispatch_group_t相關(guān)屬性介紹:

1.dispatch_group_async(group, queue, block);
將block任務(wù)添加到queue隊列,并被group組管理

2.dispatch_group_enter(group);
聲明dispatch_group_enter(group)下面的任務(wù)由group組管理,group組的任務(wù)數(shù)+1

3.dispatch_group_leave(group);
相應(yīng)的任務(wù)執(zhí)行完成,group組的任務(wù)數(shù)-1

4.dispatch_group_create();
創(chuàng)建一個group組

5.dispatch_group_wait(group1, DISPATCH_TIME_FOREVER);
當前線程暫停,等待dispatch_group_wait(group1, DISPATCH_TIME_FOREVER)上面的任務(wù)執(zhí)行完成后,線程才繼續(xù)執(zhí)行。

6.dispatch_group_notify(group1, queue1,block);
監(jiān)聽group組中任務(wù)的完成狀態(tài),當所有的任務(wù)都執(zhí)行完成后,觸發(fā)block塊,執(zhí)行總結(jié)性處理。

(2)常見用法的區(qū)別
在使用group組處理任務(wù)時,常見的有兩種組合。

其一:

dispatch_group_async(group, queue, block);

dispatch_group_notify(group1, queue1, block);

在這種組合下,根據(jù)任務(wù)是同步、異步又分為兩種,這兩種組合的執(zhí)行代碼與運行結(jié)果如下:

同步任務(wù)時:

- (void)groupSync{
    dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group1 = dispatch_group_create();
    dispatch_group_async(group1, queue1, ^{
        dispatch_sync(queue1, ^{
            for(NSInteger i =0; i<3; i++) {
                NSLog(@"同步任務(wù)1-:%ld",(long)i);
            }
        });
    });
    dispatch_group_async(group1, queue1, ^{
        dispatch_sync(queue1, ^{
            for(NSInteger i =0; i<3; i++) {
                sleep(1);
                NSLog(@"同步任務(wù)2-:%ld",(long)i);
            }
        });
    });
    //等待上面的任務(wù)全部完成后,會收到通知執(zhí)行block中的代碼 (不會阻塞線程)
    dispatch_group_notify(group1, queue1, ^{
        NSLog(@"全部任務(wù)執(zhí)行完成");
    });
}
group-同步

異步任務(wù)時:

- (void)groupAsync{
    dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group1 = dispatch_group_create();
    dispatch_group_async(group1, queue1, ^{
        dispatch_async(queue1, ^{
            for(NSInteger i =0; i<3; i++) {
                sleep(1);
                NSLog(@"異步任務(wù)1,%ld",(long)i);
            }
        });
    });
    dispatch_group_async(group1, queue1, ^{
        dispatch_async(queue1, ^{
            for(NSInteger i =0; i<3; i++) {
                sleep(1);
                NSLog(@"異步任務(wù)2,%ld",(long)i);
            }
        });
    });
    //等待上面的任務(wù)全部完成后,會收到通知執(zhí)行block中的代碼 (不會阻塞線程)
    dispatch_group_notify(group1, queue1, ^{
        NSLog(@"全部任務(wù)執(zhí)行完成");
    });
}
group-異步

結(jié)論:dispatch_group_async(group, queue, block) 和 dispatch_group_notify(group1, queue1, block) 組合在執(zhí)行同步任務(wù)時正常,在執(zhí)行異步任務(wù)時不正常。

其二:

dispatch_group_enter(group);

dispatch_group_leave(group);

dispatch_group_notify(group1, queue1,block);

同樣的在這種組合下,根據(jù)任務(wù)是同步、異步也分為兩種,這兩種組合的執(zhí)行代碼與運行結(jié)果如下:

同步任務(wù)時:

- (void)groupSyncEnter {
    dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group2 = dispatch_group_create();
    dispatch_group_enter(group2);
    dispatch_sync(queue2, ^{
        for(NSInteger i =0; i<3; i++) {
            NSLog(@"同步任務(wù)1,%ld",(long)i);
        }
        dispatch_group_leave(group2);
    });
    dispatch_group_enter(group2);
    dispatch_sync(queue2, ^{
        for(NSInteger i =0; i<3; i++) {
            NSLog(@"同步任務(wù)2,%ld",(long)i);
        }
        dispatch_group_leave(group2);
    });
    //等待上面的任務(wù)全部完成后,會收到通知執(zhí)行block中的代碼 (不會阻塞線程)
    dispatch_group_notify(group2, queue2, ^{
        NSLog(@"全部任務(wù)執(zhí)行完成");
    });
}
group-同步

異步任務(wù)時:

- (void)groupAsyncEnter{
    dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group2 = dispatch_group_create();
    dispatch_group_enter(group2);
    dispatch_async(queue2, ^{
        for(NSInteger i =0; i<3; i++) {
            NSLog(@"異步任務(wù)1,%ld",(long)i);
        }
        dispatch_group_leave(group2);
    });
    dispatch_group_enter(group2);
    dispatch_async(queue2, ^{
        for(NSInteger i =0; i<3; i++) {
            NSLog(@"異步任務(wù)2,%ld",(long)i);
        }
        dispatch_group_leave(group2);
    });
    //等待上面的任務(wù)全部完成后,會收到通知執(zhí)行block中的代碼 (不會阻塞線程)
    dispatch_group_notify(group2, queue2, ^{
        NSLog(@"全部任務(wù)執(zhí)行完成");
    });
}
group-異步

結(jié)論:dispatch_group_enter(group)、dispatch_group_leave(group) 和dispatch_group_notify(group, queue,block) 組合在執(zhí)行同步任務(wù)時正常,在執(zhí)行異步任務(wù)時正常。

  • 5.5 GCD一些常用API
    除了以上說道的api之外,還有一些常用CGD的api:
/**兩個作用:
第一,變更隊列的執(zhí)行優(yōu)先級;
第二,目標隊列可以成為原隊列的執(zhí)行階層。*/
dispatch_set_target_queue 

 /**想在指定時間后執(zhí)行處理的情況,可使用dispatch_after函數(shù)來實現(xiàn)。
需要注意的是,dispatch_after函數(shù)并不是在指定時間后執(zhí)行處理,
而只是在指定時間追加處理到Dispatch Queue。*/
dispatch_after

dispatch_apply//第一個參數(shù)為重復(fù)次數(shù),第二個參數(shù)為追加對象的Dispatch,第三個參數(shù)為追加的處理
dispatch_apply(10, queue, ^(size_t index) {
    NSLog(@"%zu",index);
});

dispatch_suspend(queue)//函數(shù)掛起指定的Dispatch Queue

dispatch_resume(queue)//函數(shù)恢復(fù)指定的Dispatch Queue
 
dispatch_once//函數(shù)是保證在應(yīng)用程序執(zhí)行中只執(zhí)行一次指定處理的API
  • 5.6 GCD終止線程
    如果還未執(zhí)行的子線程可以用dispatch_block_cancel來取消(需要注意必須用dispatch_block_create創(chuàng)建dispatch_block_t)
- (void)stopSync{
    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
    dispatch_block_t block1 = dispatch_block_create(0, ^{
        NSLog(@"block1 begin");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"block1 end");
    });

    dispatch_block_t block2 = dispatch_block_create(0, ^{
        NSLog(@"block2 ");
    });
    dispatch_async(queue, block1);
    dispatch_async(queue, block2);
    //取消執(zhí)行block2
    dispatch_block_cancel(block2);
}

運行結(jié)果如下:

image

但是,如果執(zhí)行中的線程,我們該怎么讓其退出呢?方法如下:

- (void)stopAsync {
    __block BOOL isFinish =NO;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for(long i=0; i<10000; i++) {
            NSLog(@"正在執(zhí)行第 %ld 次",i);
            sleep(1);
            if(isFinish ==YES) {
                NSLog(@"已經(jīng)終止了");
                return;
            }
        };
    });
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(10 * NSEC_PER_SEC)),dispatch_get_main_queue(), ^{
        NSLog(@"我要停止啦");
        isFinish =YES;
    });
}

執(zhí)行結(jié)果:

image

方法很簡單,線程外設(shè)置__block變量,配合線程中return結(jié)束。


六、NSOperation的理解與使用

6.1 NSOperation, NSOperationQueue 簡介

NSOperation需要配合NSOperationQueue來實現(xiàn)多線程;NSOperation,NSOperationQueue 是蘋果提供給我們的一套多線程解決方案,是基于 GCD 更高一層的封裝,完全面向?qū)ο蟆5潜?GCD 更簡單易用,代碼可讀性更高。

既然是基于 GCD 的更高一層的封裝。那么,GCD 的一些概念同樣適用于 NSOperation,NSOperationQueue。在 NSOperation,NSOperationQueue 中也有類似于任務(wù)(操作)和隊列(操作隊列)的概念。

  • 操作(Operation)
    • 執(zhí)行操作的意思,即在線程中執(zhí)行的那段代碼。在 GCD 中是放在 block 中的。在 NSOperation 中,我們使用 NSOperation 子類NSInvocationOperation,NSBlockOperation,或者時自定義子類來封裝操作。
  • 操作隊列(Operation Queue)
    • 這里的隊列指操作隊列,即用來存放操作的隊列。不同于 GCD中的調(diào)度隊列 FIFO(先進先出)的原則。NSOperationQueue 對于添加到隊列中的操作,首先進入準備就緒的狀態(tài)(就緒狀態(tài)取決于操作之間的依賴關(guān)系),然后進入就緒狀態(tài)的操作的開始執(zhí)行順序(非結(jié)束執(zhí)行順序),由操作之間相對的優(yōu)先級決定(優(yōu)先級是操作對象自身的屬性);
    • 操作隊列通過設(shè)置最大并發(fā)操作數(shù)(maxConcurrentOperationCount)來控制并發(fā),串行;
    • NSOperationQueue 為我們提供了兩種不同類型的隊列:主隊列和自定義隊列。主隊列運行在主線程之上,而自定義隊列在后臺執(zhí)行。

NSOperation實現(xiàn)多線程的步驟如下:

  1. 創(chuàng)建操作:先將要執(zhí)行的操作封裝到一個 NSOperation 對象中
  2. 創(chuàng)建隊列:創(chuàng)建 NSOperationQueue 對象。
  3. 將操作加入到隊列中:將 NSOpeartion 對象添加到 NSOperationQueue 對象中。

需要注意的是,NSOperation是個抽象類,實際運用時中需要使用它的子類,有三種方式:

  1. 使用子類NSInvocationOperation
  2. 使用子類NSBlockOperation
  3. 定義繼承自NSOperation的子類,通過實現(xiàn)內(nèi)部相應(yīng)的方法來封裝任務(wù)。
6.2 NSOperation的三種創(chuàng)建方式
6.2.1 使用NSInvocationOperation創(chuàng)建
//NSInvocationOperation使用
- (void)useNSInvocationOperation{
    //創(chuàng)建NSInvocationOperation并關(guān)聯(lián)方法
    NSInvocationOperation* opratin = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperationAction) object:nil];
    //執(zhí)行操作
    [opratin start];
}
- (void)invocationOperationAction{
    NSLog(@"NSInvocationOperation任務(wù),沒有加入隊列==%@", [NSThread currentThread]);
}

執(zhí)行結(jié)果如下,得到結(jié)論:程序在主線程執(zhí)行,沒有開啟新線程。這是因為 NSOperation 多線程的使用需要配合隊列 NSOperationQueue ,在不使用 NSOperationQueue ,單獨使用 NSOperation 的情況下,系統(tǒng)同步執(zhí)行操作。

執(zhí)行結(jié)果

6.2.2 使用NSBlockOperation創(chuàng)建
//NSBlockOperation使用
- (void)useNSBlockOperation{
    //創(chuàng)建NSBlockOperation并把任務(wù)放到block中
    NSBlockOperation* operation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"NSInvocationOperation任務(wù),沒有加入隊列==%@", [NSThread currentThread]);
    }];
    //執(zhí)行操作
    [operation start];
}

執(zhí)行結(jié)果如下,結(jié)論上同,不在闡述。


執(zhí)行結(jié)果

但是NSBlockOperation有一個方法addExecutionBlock:,通過這個方法可以讓NSBlockOperation實現(xiàn)多線程。代碼如下:

//NSBlockOperation使用
- (void)useNSBlockOperation{
    //創(chuàng)建NSBlockOperation并把任務(wù)放到block中
    NSBlockOperation* operation = [NSBlockOperation blockOperationWithBlock:^{
        for(int i = 0; i < 3; i++){
            NSLog(@"NSInvocationOperation任務(wù)==%@", [NSThread currentThread]);
        }
//        NSLog(@"NSInvocationOperation任務(wù),沒有加入隊列==%@", [NSThread currentThread]);
    }];
    [operation addExecutionBlock:^{
        for(int i = 0; i < 3; i++){
            NSLog(@"addExecutionBlock方法添加任務(wù)1==%@", [NSThread currentThread]);
        }
    }];
    [operation addExecutionBlock:^{
        for(int i = 0; i < 3; i++){
            NSLog(@"addExecutionBlock方法添加任務(wù)2==%@", [NSThread currentThread]);
        }
    }];
    [operation addExecutionBlock:^{
        for(int i = 0; i < 3; i++){
            NSLog(@"addExecutionBlock方法添加任務(wù)3==%@", [NSThread currentThread]);
        }
    }];
    //執(zhí)行操作
    [operation start];
}

執(zhí)行結(jié)果如下,(第二個執(zhí)行結(jié)果是添加了十個操作,執(zhí)行了兩次出現(xiàn)的結(jié)果...)


執(zhí)行結(jié)果1

執(zhí)行結(jié)果2

得到結(jié)論:

  • 通過addExecutionBlock可以為 NSBlockOperation 添加額外的操作。這些操作(包括blockOperationWithBlock中的操作)可以再不同的線程中同時(并發(fā))執(zhí)行。只有當所有相關(guān)的操作已經(jīng)執(zhí)行完成時,才視為完成。
  • 如果添加的操作多的話,blockOperationWithBlock:中的操作也可能會在其他線程(非當前線程)中執(zhí)行,這是由系統(tǒng)決定的,并不是說添加到blockOperationWithBlock:中的操作一定會在當前線程中執(zhí)行。(第二張執(zhí)行結(jié)果,好奇的可以試試,嘿嘿...)
  • NSBlockOperation是否會開啟新線程,取決于操作的個數(shù)。如果添加的操作的個數(shù)多,就會開啟新線程。開啟的線程數(shù)是由系統(tǒng)來決定的。
6.2.3 使用繼承自NSOperation的子類創(chuàng)建

首先我們定義一個繼承自NSOperation的類,然后重寫它的main方法,之后就可以使用這個子類來進行相關(guān)的操作了。

/*******************"OurNSOperation.h"*************************/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface OurNSOperation : NSOperation
@end
NS_ASSUME_NONNULL_END

/*******************"OurNSOperation.m"*************************/
#import "OurNSOperation.h"
@implementation OurNSOperation
- (void)main{
    for(int i = 0; i < 3; i++){
        NSLog(@"NSOperation的子類OurNSOperation==%@",[NSThread currentThread]);
    }
}
@end
/*******"回到主控制器導(dǎo)入頭文件并使用OurNSOperation"********/
- (void)useOurNSOperation{
    //創(chuàng)建OurNSOperation對象
    OurNSOperation* operation = [[OurNSOperation alloc] init];
    //開始執(zhí)行
    [operation start];
}

運行結(jié)果如下,依然是在主線程執(zhí)行。所以,NSOperation是需要配合隊列NSOperationQueue來實現(xiàn)多線程的。


執(zhí)行結(jié)果
6.3 隊列NSOperationQueue

NSOperationQueue只有兩種隊列:主隊列、自定義隊列。自定義隊列包含了串行和并發(fā)。

  • 主隊列的創(chuàng)建如下:主隊列上的任務(wù)是在主線程執(zhí)行的。
    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];

  • 自定義隊列(非主隊列)的創(chuàng)建如下:加入到自定義隊列中的任務(wù),默認就是并發(fā),開啟多線程。
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

  • 需要注意的是:

    • 非主隊列(自定義隊列)可以實現(xiàn)串行或并行。
    • 隊列NSOperationQueue有一個參數(shù)叫做最大并發(fā)數(shù):maxConcurrentOperationCount。
    • maxConcurrentOperationCount默認為-1,直接并發(fā)執(zhí)行,所以加入到‘非主隊列’中的任務(wù)默認就是并發(fā),開啟多線程。
    • 當maxConcurrentOperationCount為1時,則表示不開線程,也就是串行。
    • 當maxConcurrentOperationCount大于1時,進行并發(fā)執(zhí)行。
    • 系統(tǒng)對最大并發(fā)數(shù)有一個限制,所以即使程序員把maxConcurrentOperationCount設(shè)置的很大,系統(tǒng)也會自動調(diào)整。所以把最大并發(fā)數(shù)設(shè)置的很大是沒有意義的。
6.4 NSOperation + NSOperationQueue

NSOperation 需要配合 NSOperationQueue 來實現(xiàn)多線程,所以把任務(wù)加入隊列,這才是NSOperation的常規(guī)使用方式。

6.4.1 addOperation添加任務(wù)到隊列

先創(chuàng)建好任務(wù),然后運用addOperation:<#(nonnull NSOperation *)#>方法來吧任務(wù)添加到隊列中,示例代碼如下:

- (void)useAddOperation {
    NSOperationQueue* queue = [[NSOperationQueue alloc] init];
    NSInvocationOperation* operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperationActions) object:nil];
    NSBlockOperation* blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        for(int i = 0; i < 3; i++){
            NSLog(@"NSBlockOperation任務(wù) == %@ 第%d次執(zhí)行",[NSThread currentThread],i);
        }
    }];
    
    [queue addOperation:operation];
    [queue addOperation:blockOperation];
}

- (void)invocationOperationActions {
    for (int i = 0; i < 3; i++){
        NSLog(@"NSInvocationOperation任務(wù) == %@ 第%d次執(zhí)行",[NSThread currentThread],i);
    }
}

運行結(jié)果如下,可以看出,任務(wù)都是并發(fā)在子線程執(zhí)行的,開啟了新線程。


執(zhí)行結(jié)果
6.4.2 addOperationWithBlock添加任務(wù)到隊列

這是一個更方便的把任務(wù)添加到隊列的方法,直接把任務(wù)寫在block中,添加到任務(wù)中。

- (void)useAddOperationWithBlock{
    NSOperationQueue* queue = [[NSOperationQueue alloc] init];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 3; i++){
            NSLog(@"任務(wù)1 == %@ 第%d次執(zhí)行",[NSThread currentThread],i);
        }
    }];
    
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 3; i++){
            NSLog(@"任務(wù)2 == %@ 第%d次執(zhí)行",[NSThread currentThread],i);
        }
    }];
    
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 3; i++){
            NSLog(@"任務(wù)3 == %@ 第%d次執(zhí)行",[NSThread currentThread],i);
        }
    }];
}

執(zhí)行結(jié)果如下,使用 addOperationWithBlock:將操作加入到操作隊列后能夠開啟新線程,進行并發(fā)執(zhí)行。


執(zhí)行結(jié)果
6.4.3 maxConcurrentOperationCount控制串行執(zhí)行、并發(fā)執(zhí)行

maxConcurrentOperationCount,叫做最大并發(fā)操作數(shù),用來控制一個特定隊列中可以有多少個操作同時參與并發(fā)執(zhí)行。

注意:這里maxConcurrentOperationCount控制的不是并發(fā)線程的數(shù)量,而是一個隊列中同時能并發(fā)執(zhí)行的最大操作數(shù)。而且一個操作也并非只能在一個線程中運行。(6.3中做過一些介紹,不再闡述)

- (void)useMaxConcurrentOperationCount{
    NSOperationQueue* queue = [[NSOperationQueue alloc] init];
    
    //設(shè)置并發(fā)數(shù)
    queue.maxConcurrentOperationCount = 1;//串行隊列
//    queue.maxConcurrentOperationCount = 4;//并行隊列
//    queue.maxConcurrentOperationCount = 100;//并行隊列
    
    // 使用 addOperationWithBlock添加操作到隊列中
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            [NSThread sleepForTimeInterval:1]; // 模擬耗時操作
            NSLog(@"任務(wù)1 == %@ 第%d次執(zhí)行",[NSThread currentThread],i); // 打印當前線程
        }
    }];
    
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            [NSThread sleepForTimeInterval:1]; // 模擬耗時操作
            NSLog(@"任務(wù)2 == %@ 第%d次執(zhí)行",[NSThread currentThread],i); // 打印當前線程
        }
    }];
    
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            [NSThread sleepForTimeInterval:1]; // 模擬耗時操作
            NSLog(@"任務(wù)3 == %@ 第%d次執(zhí)行",[NSThread currentThread],i); // 打印當前線程
        }
    }];
    
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            [NSThread sleepForTimeInterval:1]; // 模擬耗時操作
            NSLog(@"任務(wù)4 == %@ 第%d次執(zhí)行",[NSThread currentThread],i); // 打印當前線程
        }
    }];
    
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            [NSThread sleepForTimeInterval:1]; // 模擬耗時操作
            NSLog(@"任務(wù)5 == %@ 第%d次執(zhí)行",[NSThread currentThread],i); // 打印當前線程
        }
    }];
    
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            [NSThread sleepForTimeInterval:1]; // 模擬耗時操作
            NSLog(@"任務(wù)6 == %@ 第%d次執(zhí)行",[NSThread currentThread],i); // 打印當前線程
        }
    }];
}
  • 最大并發(fā)操作數(shù)為1時


    最大并發(fā)操作數(shù)為1時
  • 最大并發(fā)操作數(shù)為4時


    最大并發(fā)操作數(shù)為4時
  • 最大并發(fā)操作數(shù)為100時


    最大并發(fā)操作數(shù)為100時
  • 可以看出:當最大并發(fā)操作數(shù)為1時,操作是按照順序串行執(zhí)行的,并且一個操作完成之后,下一個操作才開始執(zhí)行。當最大操作并發(fā)數(shù)大于1時,操作是并發(fā)執(zhí)行的,可以同時執(zhí)行多個操作。而且開啟線程數(shù)量是由系統(tǒng)決定的,不需要我們來管理。

6.5 NSOperation的操作依賴

NSOperation有一個非常好用的方法,就是操作依賴。可以從字面意思理解:某一個操作(operation2)依賴于另一個操作(operation1),只有當operation1執(zhí)行完畢,才能執(zhí)行operation2,這時,就是操作依賴大顯身手的時候了。NSOperation 提供了3個接口供我們管理和查看依賴。

  • - (void)addDependency:(NSOperation *)op;添加依賴,使當前操作依賴于操作 op 的完成
  • - (void)removeDependency:(NSOperation *)op;移除依賴,取消當前操作對象操作 op 的依賴
  • @property (readonly, copy) NSArray<NSOperation *> *dependencies;在當前操作開始執(zhí)行之前完成執(zhí)行的所有 NSOperation 對象數(shù)組。

代碼示例:A 執(zhí)行完操作,B 才能執(zhí)行操作。

- (void)useAddDependency{
    // 創(chuàng)建隊列
    NSOperationQueue* queue = [[NSOperationQueue alloc] init];
    
    queue.maxConcurrentOperationCount = 4;//并行隊列
    
    // 創(chuàng)建操作
    NSBlockOperation* A = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            [NSThread sleepForTimeInterval:1]; // 模擬耗時操作
            NSLog(@"任務(wù)A == %@ 第%d次執(zhí)行",[NSThread currentThread],i); // 打印當前線程
        }
    }];
    
    NSBlockOperation* B = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            [NSThread sleepForTimeInterval:1]; // 模擬耗時操作
            NSLog(@"任務(wù)B == %@ 第%d次執(zhí)行",[NSThread currentThread],i); // 打印當前線程
        }
    }];
    
    // 添加依賴
    [B addDependency:A];    // 讓B依賴于A,則先執(zhí)行A,再執(zhí)行B.
    
    // 添加操作到隊列中
    [queue addOperation:A];
    [queue addOperation:B];
}

執(zhí)行結(jié)果:

執(zhí)行結(jié)果

按照6.4.3所述,我們設(shè)置了queue.maxConcurrentOperationCount = 4 ;本應(yīng)并行執(zhí)行,但是設(shè)置依賴后,B等A執(zhí)行完畢后才執(zhí)行的。

6.6 NSOperation 優(yōu)先級

NSOperation 提供了queuePriority(優(yōu)先級)屬性,queuePriority 屬性適用于同一操作隊列中的操作,不適用于不同操作隊列中的操作。默認情況下,所有新創(chuàng)建的操作對象優(yōu)先級都是NSOperationQueuePriorityNormal。但是我們可以通過setQueuePriority:方法來改變當前操作在同一隊列中的執(zhí)行優(yōu)先級。

// 優(yōu)先級的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

對于添加到隊列中的操作,首先進入準備就緒的狀態(tài)(就緒狀態(tài)取決于操作之間的依賴關(guān)系),然后進入就緒狀態(tài)的操作的開始執(zhí)行順序(非結(jié)束執(zhí)行順序)由操作之間相對的優(yōu)先級決定(優(yōu)先級是操作對象自身的屬性)。

  • queuePriority 屬性決定了進入準備就緒狀態(tài)下的操作之間的開始執(zhí)行順序。并且,優(yōu)先級不能取代依賴關(guān)系。
  • 如果一個隊列中既包含高優(yōu)先級操作,又包含低優(yōu)先級操作,并且兩個操作都已經(jīng)準備就緒,那么隊列先執(zhí)行高優(yōu)先級操作。
  • 如果,一個隊列中既包含了準備就緒狀態(tài)的操作,又包含了未準備就緒的操作,未準備就緒的操作優(yōu)先級比準備就緒的操作優(yōu)先級高。那么,雖然準備就緒的操作優(yōu)先級低,也會優(yōu)先執(zhí)行。優(yōu)先級不能取代依賴關(guān)系。如果要控制操作間的啟動順序,則必須使用依賴關(guān)系。
6.7 NSOperation,NSOperationQueue 線程間的通信

在 iOS 開發(fā)過程中,我們一般在主線程里邊進行 UI 刷新,例如:點擊、滾動、拖拽等事件。我們通常把一些耗時的操作放在其他線程,比如說圖片下載、文件上傳等耗時操作。而當我們有時候在其他線程完成了耗時操作時,需要回到主線程,那么就用到了線程之間的通訊。

//線程間通信
- (void)useMainQueue {
    // 創(chuàng)建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    // 添加操作
    [queue addOperationWithBlock:^{
        // 異步執(zhí)行耗時操作
        for (int i = 0; i < 3; i++) {
            [NSThread sleepForTimeInterval:1]; // 模擬耗時操作
            NSLog(@"任務(wù)1 == %@ 第%d次執(zhí)行",[NSThread currentThread],i);
        }
        
        // 回到主線程
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:1]; // 模擬耗時操作
                NSLog(@"任務(wù)2 == %@ 第%d次執(zhí)行",[NSThread currentThread],i);
            }
        }];
    }];
}

執(zhí)行結(jié)果:


執(zhí)行結(jié)果

可以看出:通過線程間的通信,先在其他線程中執(zhí)行操作,等操作執(zhí)行完了之后再回到主線程執(zhí)行主線程的相應(yīng)操作。

6.8 NSOperation、NSOperationQueue 常用屬性和方法歸納
6.8.1 NSOperation 常用屬性和方法
  • 取消操作方法
    • - (void)cancel;可取消操作,實質(zhì)是標記 isCancelled 狀態(tài)。
  • 判斷操作狀態(tài)方法
    • - (BOOL)isFinished;判斷操作是否已經(jīng)結(jié)束。
    • - (BOOL)isCancelled; 判斷操作是否已經(jīng)標記為取消。
    • - (BOOL)isExecuting;判斷操作是否正在在運行。
    • - (BOOL)isReady;判斷操作是否處于準備就緒狀態(tài),這個值和操作的依賴關(guān)系相關(guān)。
  • 操作同步
    • - (void)waitUntilFinished; 阻塞當前線程,直到該操作結(jié)束。可用于線程執(zhí)行順序的同步。
    • - (void)setCompletionBlock:(void (^)(void))block;completionBlock 會在當前操作執(zhí)行完畢時執(zhí)行 completionBlock。
    • - (void)addDependency:(NSOperation *)op; 添加依賴,使當前操作依賴于操作 op 的完成。
    • - (void)removeDependency:(NSOperation *)op; 移除依賴,取消當前操作對操作 op 的依賴。
    • @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在當前操作開始執(zhí)行之前完成執(zhí)行的所有操作對象數(shù)組。
6.8.2 NSOperationQueue 常用屬性和方法
  • 取消/暫停/恢復(fù)操作
    • - (void)cancelAllOperations;可以取消隊列的所有操作。
    • - (BOOL)isSuspended; 判斷隊列是否處于暫停狀態(tài)。 YES 為暫停狀態(tài),NO 為恢復(fù)狀態(tài)。
    • - (void)setSuspended:(BOOL)b; 可設(shè)置操作的暫停和恢復(fù),YES 代表暫停隊列,NO 代表恢復(fù)隊列。
  • 操作同步
    • - (void)waitUntilAllOperationsAreFinished; 阻塞當前線程,直到隊列中的操作全部執(zhí)行完畢。
  • 添加/獲取操作
    • - (void)addOperationWithBlock:(void (^)(void))block;向隊列中添加一個 NSBlockOperation 類型操作對象。
    • - (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; 向隊列中添加操作數(shù)組,wait 標志是否阻塞當前線程直到所有操作結(jié)束
    • - (NSArray *)operations; 當前在隊列中的操作數(shù)組(某個操作執(zhí)行結(jié)束后會自動從這個數(shù)組清除)。
    • - (NSUInteger)operationCount; 當前隊列中的操作數(shù)。
  • 獲取隊列
    • + (id)currentQueue; 獲取當前隊列,如果當前線程不是在 NSOperationQueue 上運行則返回 nil。

    • + (id)mainQueue;獲取主隊列。

注意
這里的暫停和取消(包括操作的取消和隊列的取消)并不代表可以將當前的操作立即取消,而是當當前的操作執(zhí)行完畢之后不再執(zhí)行新的操作。
暫停和取消的區(qū)別在于:暫停操作之后還可以恢復(fù)操作,繼續(xù)向下執(zhí)行;而取消操作之后,所有的操作都清空了,無法再接著執(zhí)行剩下的操作。


總結(jié):
借鑒很多內(nèi)容,但是有些忘了是誰的,就不一一列舉了。文章有點長,希望大家能耐心的看下來,如果發(fā)現(xiàn)問題,也希望大家積極指正,如有不足,也懇請大家告知,謝謝。


參考資料


參考文章
iOS多線程全套:線程生命周期,多線程的四種解決方案,線程安全問題,GCD的使用,NSOperation的使用
iOS 多線程: [NSOperation NSOperationQueue] 詳解

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