1. 程式人生 > >iOS 多執行緒 NSOperation、NSOperationQueue

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]); // 列印當前執行緒 } } 

輸出結果:


   
  • 可以看到:在沒有使用 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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。