iOS 多線程 NSOperation、NSOperationQueue

来源:https://www.cnblogs.com/jukaiit/archive/2018/11/30/10038893.html
-Advertisement-
Play Games

1. NSOperation、NSOperationQueue 簡介 NSOperation、NSOperationQueue 是蘋果提供給我們的一套多線程解決方案。實際上 NSOperation、NSOperationQueue 是基於 GCD 更高一層的封裝,完全面向對象。但是比 GCD 更簡單 ...


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 子類 NSInvocationOperationNSBlockOperation,或者自定義子類來封裝操作。
  • 操作隊列(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]); // 列印當前線程
    }
}

輸出結果:


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

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

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

輸出結果:


   
  • 可以看到:在其他線程中單獨使用子類 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];
}

輸出結果:


   
  • 可以看到:在沒有使用 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];
}

輸出結果:


   
  • 可以看出:使用子類 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

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

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

輸出結果:


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

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

4.2 創建隊列

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

  • 主隊列
    • 凡是添加到主隊列中的操作,都會放到主線程中執行(註:不包括操作使用addExecutionBlock:添加的額外操作,額外操作可能在其他線程執行,感謝指正)。
// 主隊列獲取方法
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]
}

輸出結果:


   
  • 可以看出:使用 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]); // 列印當前線程
        }
    }];
}

輸出結果:


   
  • 可以看出:使用 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 輸出結果:


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


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

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

6. NSOperation 操作依賴

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

  • - (void)addDependency:(NSOperation *)op;添加依賴,使當前操作依賴於操作 op 的完成。
  • - (void)removeDependency:(NSOperation *)op;移除依賴,取消當前操作對操作 op 的依賴。
  • @property (readonly, copy) NSArray<NSOperation *> *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];
}

輸出結果:


   
  • 可以看到:通過添加操作依賴,無論運行幾次,其結果都是 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]); // 列印當前線程
            }
        }];
    }];
}

輸出結果:


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

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
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        [self saleTicketNotSafe];
    }];

    // 4.創建賣票操作 op2
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        [self 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;
        }
    }
}

輸出結果:


   

省略一部分結果圖。。。


   
  • 可以看到:在不考慮線程安全,不使用 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
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        [self saleTicketSafe];
    }];

    // 4.創建賣票操作 op2
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        [self 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;
        }
    }
}

輸出結果:


   

省略一部分結果圖。。。


   
  • 可以看出:在考慮了線程安全,使用 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<NSOperation *> *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;獲取主隊列。

註意:

  1. 這裡的暫停和取消(包括操作的取消和隊列的取消)並不代表可以將當前的操作立即取消,而是噹噹前的操作執行完畢之後不再執行新的操作。
  2. 暫停和取消的區別就在於:暫停操作之後還可以恢復操作,繼續向下執行;而取消操作之後,所有的操作就清空了,無法再接著執行剩下的操作。

 



作者:行走的少年郎
鏈接:https://www.jianshu.com/p/4b1d77054b35
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一.概述 除了上篇介紹的RDB持久化功能之外,Redis還提供了AOF(Append Only File)持久化功能。與RDB保存資料庫中的鍵值對來記錄資料庫狀態不同,AOF是通過保存redis伺服器所執行的寫命令來記錄資料庫狀態的。AOF持久化方式記錄每次對伺服器寫的操作,當伺服器啟動時,就會通過 ...
  • 摘要:當前研發工作中經常出現因資料庫表、資料庫表欄位格式不規則而影響開發進度的問題,在後續開發使用原來資料庫表時,也會因為資料庫表的可讀性不夠高,表欄位規則不統一,造成數據查詢,數據使用效率低的問題,所以有必要整理出一套合適的資料庫表欄位命名規範來解決優化這些問題。 本文是一篇包含了資料庫命名、數據 ...
  • mysql是一種關係型資料庫管理系統。以mysql5.7版本為例,安裝過程如下: 首先百度出mysql的官網,進入:(以下是自己安裝失敗的過程,直接下拉最後看大佬的安裝過程吧,就是那個紅紅的網址) 找到mysql的下載社區,找到對應的版本,這裡以mysql5.7為例: 這裡我們選擇zip格式安裝,對 ...
  • [20181130]如何猜測那些值存在hash衝突.txt--//今年6月份開始kerrycode的1個帖子提到子查詢結果緩存在哈希表中情況:--//鏈接:http://www.cnblogs.com/kerrycode/p/9099507.html,摘要:通俗來將,當使用標量子查詢的時候,ORAC ...
  • [20181130]hash衝突導致查詢緩慢.txt--//昨天看了鏈接https://jonathanlewis.wordpress.com/2018/11/26/shrink-space-2/,演示了Shrink Space導致--//執行語句緩慢的情況,我自己重覆測試,實際上這樣發生的概率還是 ...
  • [20181130]control file sequential read.txt--//昨天上午探究了大量控制文件讀的情況,鏈接:http://blog.itpub.net/267265/viewspace-2222146/--//今天做一些細節探究:1.環境:SYS@xxxxx1> @ ver ...
  • 1、ipa包解壓縮:右鍵.ipa包,使用【歸檔實用工具/unarchiver】打開 2、進入解壓縮後的payload目錄,右鍵ipa包 顯示包內容 3、找到info.plist文件,直接拖拽出來 4、使用plist編輯器打開 info.plist,就可以查看CFBundleDisplayName、C ...
  • 超簡單釘釘打卡破解教程 ​ 公司前幾天要用釘釘打卡 作為拖延症的我肯定天天遲到 所以就一直找辦法可以遠程打卡 什麼teamviewer 大牛定位都不是特別好用要麼要兩台手機要麼就是收費什麼的 不過前兩天找到一個不錯的軟體我目前一直在用而且還是免費的 就是需要xposed框架支持 為了能不遲到還是折騰 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...