iOS多線程:『NSOperation、NSOperationQueue』詳盡總結

1521514117925677.png

本文來自Walking Boy的博客

本文用來介紹 iOS 多線程中 NSOperation、NSOperationQueue 的相關知識以及使用方法。

1. NSOperation、NSOperationQueue 簡介

NSOperation、NSOperationQueue 是蘋果提供給我們的一套多線程解決方案。實際上 NSOperation、NSOperationQueue 是基于 GCD 更高一層的封裝,完全面向對象。但是比 GCD 更簡單易用、代碼可讀性也更高。

為什么要使用 NSOperation、NSOperationQueue?

  1. 可添加完成的代碼塊,在操作完成后執行。
  2. 添加操作之間的依賴關系,方便的控制執行順序。
  3. 設定操作執行的優先級。
  4. 可以很方便的取消一個操作的執行。
  5. 使用 KVO 觀察對操作執行狀態的更改:isExecuteing、isFinished、isCancelled。

2. NSOperation、NSOperationQueue 操作和操作隊列

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

  • 操作(Operation):

    執行操作的意思,換句話說就是你在線程中執行的那段代碼。

    在 GCD 中是放在 block 中的。在 NSOperation 中,我們使用 NSOperation 子類 NSInvocationOperation、NSBlockOperation,或者自定義子類來封裝操作。

  • 操作隊列(Operation Queues):

    這里的隊列指操作隊列,即用來存放操作的隊列。不同于 GCD 中的調度隊列 FIFO(先進先出)的原則。NSOperationQueue 對于添加到隊列中的操作,首先進入準備就緒的狀態(就緒狀態取決于操作之間的依賴關系),然后進入就緒狀態的操作的開始執行順序(非結束執行順序)由操作之間相對的優先級決定(優先級是操作對象自身的屬性)。

    操作隊列通過設置最大并發操作數(maxConcurrentOperationCount)來控制并發、串行。

    NSOperationQueue 為我們提供了兩種不同類型的隊列:主隊列和自定義隊列。主隊列運行在主線程之上,而自定義隊列在后臺執行。

3. NSOperation、NSOperationQueue 使用步驟

NSOperation 需要配合 NSOperationQueue 來實現多線程。因為默認情況下,NSOperation 單獨使用時系統同步執行操作,配合 NSOperationQueue 我們能更好的實現異步執行。

NSOperation 實現多線程的使用步驟分為三步:

  1. 創建操作:先將需要執行的操作封裝到一個 NSOperation 對象中。
  2. 創建隊列:創建 NSOperationQueue 對象。
  3. 將操作加入到隊列中:將 NSOperation 對象添加到 NSOperationQueue 對象中。

之后呢,系統就會自動將 NSOperationQueue 中的 NSOperation 取出來,在新線程中執行操作。

下面我們來學習下 NSOperation 和 NSOperationQueue 的基本使用。

4. NSOperation 和 NSOperationQueue 基本使用

4.1 創建操作

NSOperation 是個抽象類,不能用來封裝操作。我們只有使用它的子類來封裝操作。我們有三種方式來封裝操作。

  1. 使用子類 NSInvocationOperation
  2. 使用子類 NSBlockOperation
  3. 自定義繼承自 NSOperation 的子類,通過實現內部相應的方法來封裝操作。

在不使用 NSOperationQueue,單獨使用 NSOperation 的情況下系統同步執行操作,下面我們學習以下操作的三種創建方式。

4.1.1 使用子類 NSInvocationOperation

/**
 * 使用子類 NSInvocationOperation
 */
- (void)useInvocationOperation {
    // 1.創建 NSInvocationOperation 對象
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
    // 2.調用 start 方法開始執行操作
    [op start];
}
/**
 * 任務1
 */
- (void)task1 {
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
        NSLog(@"1---%@", [NSThread currentThread]); // 打印當前線程
    }
}

輸出結果:

image
  • 可以看到:在沒有使用 NSOperationQueue、在主線程中單獨使用使用子類 NSInvocationOperation 執行一個操作的情況下,操作是在當前線程執行的,并沒有開啟新線程。

如果在其他線程中執行操作,則打印結果為其他線程。

// 在其他線程使用子類 NSInvocationOperation
[NSThread detachNewThreadSelector:@selector(useInvocationOperation) toTarget:self withObject:nil];

輸出結果:

image
  • 可以看到:在其他線程中單獨使用子類 NSInvocationOperation,操作是在當前調用的其他線程執行的,并沒有開啟新線程。

下邊再來看看 NSBlockOperation。

4.1.2 使用子類 NSBlockOperation

/**
 * 使用子類 NSBlockOperation
 */
- (void)useBlockOperation {
    // 1.創建 NSBlockOperation 對象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    // 2.調用 start 方法開始執行操作
    [op start];
}

輸出結果:

image
  • 可以看到:在沒有使用 NSOperationQueue、在主線程中單獨使用 NSBlockOperation 執行一個操作的情況下,操作是在當前線程執行的,并沒有開啟新線程。

注意:和上邊 NSInvocationOperation 使用一樣。因為代碼是在主線程中調用的,所以打印結果為主線程。如果在其他線程中執行操作,則打印結果為其他線程。

但是,NSBlockOperation 還提供了一個方法 addExecutionBlock:,通過 addExecutionBlock: 就可以為 NSBlockOperation 添加額外的操作。這些操作(包括 blockOperationWithBlock 中的操作)可以在不同的線程中同時(并發)執行。只有當所有相關的操作已經完成執行時,才視為完成。

如果添加的操作多的話,blockOperationWithBlock: 中的操作也可能會在其他線程(非當前線程)中執行,這是由系統決定的,并不是說添加到 blockOperationWithBlock: 中的操作一定會在當前線程中執行。(可以使用 addExecutionBlock: 多添加幾個操作試試)。

/**
 * 使用子類 NSBlockOperation
 * 調用方法 AddExecutionBlock:
 */
- (void)useBlockOperationAddExecutionBlock {
    // 1.創建 NSBlockOperation 對象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    // 2.添加額外的操作
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"2---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"3---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"4---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"5---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"6---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"7---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"8---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    // 3.調用 start 方法開始執行操作
    [op start];
}

輸出結果:

image
  • 可以看出:使用子類 NSBlockOperation,并調用方法 AddExecutionBlock: 的情況下,blockOperationWithBlock:方法中的操作 和 addExecutionBlock: 中的操作是在不同的線程中異步執行的。而且,這次執行結果中 blockOperationWithBlock:方法中的操作也不是在當前線程(主線程)中執行的。從而印證了blockOperationWithBlock: 中的操作也可能會在其他線程(非當前線程)中執行。

一般情況下,如果一個 NSBlockOperation 對象封裝了多個操作。NSBlockOperation 是否開啟新線程,取決于操作的個數。如果添加的操作的個數多,就會自動開啟新線程。當然開啟的線程數是由系統來決定的。

4.1.3 使用自定義繼承自 NSOperation 的子類

如果使用子類 NSInvocationOperation、NSBlockOperation 不能滿足日常需求,我們可以使用自定義繼承自 NSOperation 的子類。可以通過重寫 main 或者 start 方法 來定義自己的 NSOperation 對象。重寫main方法比較簡單,我們不需要管理操作的狀態屬性 isExecuting 和 isFinished。當 main 執行完返回的時候,這個操作就結束了。

先定義一個繼承自 NSOperation 的子類,重寫main方法。

// YSCOperation.h 文件
#import <foundation foundation.h="">
@interface YSCOperation : NSOperation
@end
// YSCOperation.m 文件
#import "YSCOperation.h"
@implementation YSCOperation
- (void)main {
    if (!self.isCancelled) {
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@", [NSThread currentThread]);
        }
    }
}
@end</foundation>

然后使用的時候導入頭文件YSCOperation.h。

/**
 * 使用自定義繼承自 NSOperation 的子類
 */
- (void)useCustomOperation {
    // 1.創建 YSCOperation 對象
    YSCOperation *op = [[YSCOperation alloc] init];
    // 2.調用 start 方法開始執行操作
    [op start];
}

輸出結果:

image
  • 可以看出:在沒有使用 NSOperationQueue、在主線程單獨使用自定義繼承自 NSOperation 的子類的情況下,是在主線程執行操作,并沒有開啟新線程。

下邊我們來講講 NSOperationQueue 的創建。

4.2 創建隊列

NSOperationQueue 一共有兩種隊列:主隊列、自定義隊列。其中自定義隊列同時包含了串行、并發功能。下邊是主隊列、自定義隊列的基本創建方法和特點。

  • 主隊列

    凡是添加到主隊列中的操作,都會放到主線程中執行。

    // 主隊列獲取方法
    

NSOperationQueue *queue = [NSOperationQueue mainQueue];
```

  • 自定義隊列(非主隊列)

    添加到這種隊列中的操作,就會自動放到子線程中執行。

    同時包含了:串行、并發功能。

    // 自定義隊列創建方法
    

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
```

4.3 將操作加入到隊列中

上邊我們說到 NSOperation 需要配合 NSOperationQueue 來實現多線程。

那么我們需要將創建好的操作加入到隊列中去。總共有兩種方法:

  1. - (void)addOperation:(NSOperation *)op;

    需要先創建操作,再將創建好的操作加入到創建好的隊列中去。

    /**
 * 使用 addOperation: 將操作加入到操作隊列中
 */
- (void)addOperationToQueue {
    // 1.創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 2.創建操作
    // 使用 NSInvocationOperation 創建操作1
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
    // 使用 NSInvocationOperation 創建操作2
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];
    // 使用 NSBlockOperation 創建操作3
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"3---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    [op3 addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"4---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    // 3.使用 addOperation: 添加所有操作到隊列中
     [queue addOperation:op1]; // [op1 start]
    [queue addOperation:op2]; // [op2 start]
    [queue addOperation:op3]; // [op3 start]
}

輸出結果:

image
  • 可以看出:使用 NSOperation 子類創建操作,并使用 addOperation: 將操作加入到操作隊列后能夠開啟新線程,進行并發執行。
  1. - (void)addOperationWithBlock:(void (^)(void))block;

    無需先創建操作,在 block 中添加操作,直接將包含操作的 block 加入到隊列中。

/**
 * 使用 addOperationWithBlock: 將操作加入到操作隊列中
 */
- (void)addOperationWithBlockToQueue {
    // 1.創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 2.使用 addOperationWithBlock: 添加操作到隊列中
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"2---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"3---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
}

輸出結果:

image
  • 可以看出:使用 addOperationWithBlock: 將操作加入到操作隊列后能夠開啟新線程,進行并發執行。

5. NSOperationQueue 控制串行執行、并發執行

之前我們說過,NSOperationQueue 創建的自定義隊列同時具有串行、并發功能,上邊我們演示了并發功能,那么他的串行功能是如何實現的?

這里有個關鍵屬性 maxConcurrentOperationCount,叫做最大并發操作數。用來控制一個特定隊列中可以有多少個操作同時參與并發執行。

注意:這里 maxConcurrentOperationCount 控制的不是并發線程的數量,而是一個隊列中同時能并發執行的最大操作數。而且一個操作也并非只能在一個線程中運行。

  • 最大并發操作數:maxConcurrentOperationCount

    maxConcurrentOperationCount 默認情況下為-1,表示不進行限制,可進行并發執行。
    maxConcurrentOperationCount 為1時,隊列為串行隊列。只能串行執行。
    maxConcurrentOperationCount 大于1時,隊列為并發隊列。操作并發執行,當然這個值不應超過系統限制,即使自己設置一個很大的值,系統也會自動調整為 min{自己設定的值,系統設定的默認最大值}。

/**
 * 設置 MaxConcurrentOperationCount(最大并發操作數)
 */
- (void)setMaxConcurrentOperationCount {
    // 1.創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 2.設置最大并發操作數
    queue.maxConcurrentOperationCount = 1; // 串行隊列
// queue.maxConcurrentOperationCount = 2; // 并發隊列
// queue.maxConcurrentOperationCount = 8; // 并發隊列
    // 3.添加操作
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"2---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"3---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"4---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
}

最大并發操作數為1 輸出結果:

image

最大并發操作數為2 輸出結果:

image
  • 可以看出:當最大并發操作數為1時,操作是按順序串行執行的,并且一個操作完成之后,下一個操作才開始執行。當最大操作并發數為2時,操作是并發執行的,可以同時執行兩個操作。而開啟線程數量是由系統決定的,不需要我們來管理。

這樣看來,是不是比 GCD 還要簡單了許多?

6. NSOperation 操作依賴

NSOperation、NSOperationQueue 最吸引人的地方是它能添加操作之間的依賴關系。通過操作依賴,我們可以很方便的控制操作之間的執行先后順序。NSOperation 提供了3個接口供我們管理和查看依賴。

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

當然,我們經常用到的還是添加依賴操作。現在考慮這樣的需求,比如說有 A、B 兩個操作,其中 A 執行完操作,B 才能執行操作。

如果使用依賴來處理的話,那么就需要讓操作 B 依賴于操作 A。具體代碼如下:

/**
 * 操作依賴
 * 使用方法:addDependency:
 */
- (void)addDependency {
    // 1.創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 2.創建操作
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"2---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    // 3.添加依賴
    [op2 addDependency:op1]; // 讓op2 依賴于 op1,則先執行op1,在執行op2
    // 4.添加操作到隊列中
    [queue addOperation:op1];
    [queue addOperation:op2];
}

輸出結果:

image
  • 可以看到:通過添加操作依賴,無論運行幾次,其結果都是 op1 先執行,op2 后執行。

7. NSOperation 優先級

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

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

上邊我們說過:對于添加到隊列中的操作,首先進入準備就緒的狀態(就緒狀態取決于操作之間的依賴關系),然后進入就緒狀態的操作的開始執行順序(非結束執行順序)由操作之間相對的優先級決定(優先級是操作對象自身的屬性)。

那么,什么樣的操作才是進入就緒狀態的操作呢?

  • 當一個操作的所有依賴都已經完成時,操作對象通常會進入準備就緒狀態,等待執行。

舉個例子,現在有4個優先級都是 NSOperationQueuePriorityNormal(默認級別)的操作:op1,op2,op3,op4。其中 op3 依賴于 op2,op2 依賴于 op1,即 op3 -> op2 -> op1。現在將這4個操作添加到隊列中并發執行。

  • 因為 op1 和 op4 都沒有需要依賴的操作,所以在 op1,op4 執行之前,就是出于準備就緒狀態的操作。
  • 而 op3 和 op2 都有依賴的操作(op3 依賴于 op2,op2 依賴于 op1),所以 op3 和 op2 都不是準備就緒狀態下的操作。

理解了進入就緒狀態的操作,那么我們就理解了queuePriority 屬性的作用對象。

  • queuePriority 屬性決定了進入準備就緒狀態下的操作之間的開始執行順序。并且,優先級不能取代依賴關系。
  • 如果一個隊列中既包含高優先級操作,又包含低優先級操作,并且兩個操作都已經準備就緒,那么隊列先執行高優先級操作。比如上例中,如果 op1 和 op4 是不同優先級的操作,那么就會先執行優先級高的操作。
  • 如果,一個隊列中既包含了準備就緒狀態的操作,又包含了未準備就緒的操作,未準備就緒的操作優先級比準備就緒的操作優先級高。那么,雖然準備就緒的操作優先級低,也會優先執行。優先級不能取代依賴關系。如果要控制操作間的啟動順序,則必須使用依賴關系。

8. NSOperation、NSOperationQueue 線程間的通信

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

/**
 * 線程間通信
 */
- (void)communication {
    // 1.創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    // 2.添加操作
    [queue addOperationWithBlock:^{
        // 異步進行耗時操作
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]); // 打印當前線程
        }
        // 回到主線程
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            // 進行一些 UI 刷新等操作
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
                NSLog(@"2---%@", [NSThread currentThread]); // 打印當前線程
            }
        }];
    }];
}

輸出結果:

image
  • 可以看到:通過線程間的通信,先在其他線程中執行操作,等操作執行完了之后再回到主線程執行主線程的相應操作。

9. NSOperation、NSOperationQueue 線程同步和線程安全

  • 線程安全:如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作(更改變量),一般都需要考慮線程同步,否則的話就可能影響線程安全。

  • 線程同步:可理解為線程 A 和 線程 B 一塊配合,A 執行到一定程度時要依靠線程 B 的某個結果,于是停下來,示意 B 運行;B 依言執行,再將結果給 A;A 再繼續操作。

舉個簡單例子就是:兩個人在一起聊天。兩個人不能同時說話,避免聽不清(操作沖突)。等一個人說完(一個線程結束操作),另一個再說(另一個線程再開始操作)。

下面,我們模擬火車票售賣的方式,實現 NSOperation 線程安全和解決線程同步問題。

場景:總共有50張火車票,有兩個售賣火車票的窗口,一個是北京火車票售賣窗口,另一個是上海火車票售賣窗口。兩個窗口同時售賣火車票,賣完為止。

9.1 NSOperation、NSOperationQueue 非線程安全

先來看看不考慮線程安全的代碼:

/**
 * 非線程安全:不使用 NSLock
 * 初始化火車票數量、賣票窗口(非線程安全)、并開始賣票
 */
- (void)initTicketStatusNotSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
    self.ticketSurplusCount = 50;
    // 1.創建 queue1,queue1 代表北京火車票售賣窗口
    NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
    queue1.maxConcurrentOperationCount = 1;
    // 2.創建 queue2,queue2 代表上海火車票售賣窗口
    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
    queue2.maxConcurrentOperationCount = 1;
    // 3.創建賣票操作 op1
    __weak typeof(self) weakSelf = self;
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        [weakSelf saleTicketNotSafe];
    }];
    // 4.創建賣票操作 op2
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        [weakSelf saleTicketNotSafe];
    }];
    // 5.添加操作,開始賣票
    [queue1 addOperation:op1];
    [queue2 addOperation:op2];
}
/**
 * 售賣火車票(非線程安全)
 */
- (void)saleTicketNotSafe {
    while (1) {
        if (self.ticketSurplusCount > 0) {
            //如果還有票,繼續售賣
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票數:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else {
            NSLog(@"所有火車票均已售完");
            break;
        }
    }
}

輸出結果:

image

……

image
  • 可以看到:在不考慮線程安全,不使用 NSLock 情況下,得到票數是錯亂的,這樣顯然不符合我們的需求,所以我們需要考慮線程安全問題。

9.2 NSOperation、NSOperationQueue 非線程安全

線程安全解決方案:可以給線程加鎖,在一個線程執行該操作的時候,不允許其他線程進行操作。iOS 實現線程加鎖有很多種方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/ge等等各種方式。這里我們使用 NSLock 對象來解決線程同步問題。NSLock 對象可以通過進入鎖時調用 lock 方法,解鎖時調用 unlock 方法來保證線程安全。

考慮線程安全的代碼:

/**
 * 線程安全:使用 NSLock 加鎖
 * 初始化火車票數量、賣票窗口(線程安全)、并開始賣票
 */
- (void)initTicketStatusSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當前線程
    self.ticketSurplusCount = 50;
    self.lock = [[NSLock alloc] init];  // 初始化 NSLock 對象
    // 1.創建 queue1,queue1 代表北京火車票售賣窗口
    NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
    queue1.maxConcurrentOperationCount = 1;
    // 2.創建 queue2,queue2 代表上海火車票售賣窗口
    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
    queue2.maxConcurrentOperationCount = 1;
    // 3.創建賣票操作 op1
    __weak typeof(self) weakSelf = self;
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        [weakSelf saleTicketSafe];
    }];
    // 4.創建賣票操作 op2
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        [weakSelf saleTicketSafe];
    }];
    // 5.添加操作,開始賣票
    [queue1 addOperation:op1];
    [queue2 addOperation:op2];
}
/**
 * 售賣火車票(線程安全)
 */
- (void)saleTicketSafe {
    while (1) {
        // 加鎖
        [self.lock lock];
        if (self.ticketSurplusCount > 0) {
            //如果還有票,繼續售賣
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票數:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        }
        // 解鎖
        [self.lock unlock];
        if (self.ticketSurplusCount <= 0) {
            NSLog(@"所有火車票均已售完");
            break;
        }
    }
}

輸出結果:

image

……

image
  • 可以看出:在考慮了線程安全,使用 NSLock 加鎖、解鎖機制的情況下,得到的票數是正確的,沒有出現混亂的情況。我們也就解決了多個線程同步的問題。

10. NSOperation、NSOperationQueue 常用屬性和方法歸納

10.1 NSOperation 常用屬性和方法

  1. 取消操作方法

    - (void)cancel; 可取消操作,實質是標記 isCancelled 狀態。

  2. 判斷操作狀態方法

    - (BOOL)isFinished; 判斷操作是否已經結束。

    - (BOOL)isCancelled; 判斷操作是否已經標記為取消。

    - (BOOL)isExecuting; 判斷操作是否正在在運行。

    - (BOOL)isReady; 判斷操作是否處于準備就緒狀態,這個值和操作的依賴關系相關。

  3. 操作同步

    - (void)waitUntilFinished; 阻塞當前線程,直到該操作結束。可用于線程執行順序的同步。

    - (void)setCompletionBlock:(void (^)(void))block; completionBlock 會在當前操作執行完畢時執行 completionBlock。

    - (void)addDependency:(NSOperation *)op; 添加依賴,使當前操作依賴于操作 op 的完成。

    - (void)removeDependency:(NSOperation *)op; 移除依賴,取消當前操作對操作 op 的依賴。

    @property (readonly, copy) NSArray *dependencies; 在當前操作開始執行之前完成執行的所有操作對象數組。

10.2 NSOperationQueue 常用屬性和方法

  1. 取消/暫停/恢復操作

    - (void)cancelAllOperations; 可以取消隊列的所有操作。

    - (BOOL)isSuspended; 判斷隊列是否處于暫停狀態。 YES 為暫停狀態,NO 為恢復狀態。

    - (void)setSuspended:(BOOL)b; 可設置操作的暫停和恢復,YES 代表暫停隊列,NO 代表恢復隊列。

  2. 操作同步

    - (void)waitUntilAllOperationsAreFinished; 阻塞當前線程,直到隊列中的操作全部執行完畢。

  3. 添加/獲取操作

    - (void)addOperationWithBlock:(void (^)(void))block; 向隊列中添加一個 NSBlockOperation 類型操作對象。

    - (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; 向隊列中添加操作數組,wait 標志是否阻塞當前線程直到所有操作結束

    - (NSArray *)operations; 當前在隊列中的操作數組(某個操作執行結束后會自動從這個數組清除)。

    - (NSUInteger)operationCount; 當前隊列中的操作數。

  4. 獲取隊列

    + (id)currentQueue; 獲取當前隊列,如果當前線程不是在 NSOperationQueue 上運行則返回 nil。

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

注意:

這里的暫停和取消(包括操作的取消和隊列的取消)并不代表可以將當前的操作立即取消,而是當當前的操作執行完畢之后不再執行新的操作。

暫停和取消的區別就在于:暫停操作之后還可以恢復操作,繼續向下執行;而取消操作之后,所有的操作就清空了,無法再接著執行剩下的操作。

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

推薦閱讀更多精彩內容