總結NSOperation、NSOperationQueue

NSOperation、NSOperationQueue
是蘋果提供給咱們的一套多線程解決方案。
是基於 GCD 更高一層的封裝,徹底面向對象。可是比 GCD 更簡單易用、代碼可讀性也更高。數組


爲何要使用 NSOperation、NSOperationQueue?安全

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


  • 操做(Operation):
    • 執行操做的意思,換句話說就是你在線程中執行的那段代碼
    • 在 GCD 中是放在 block 中的。在 NSOperation 中,咱們使用 NSOperation 子類 NSInvocationOperationNSBlockOperation,或者自定義子類來封裝操做。
  •       操做隊列(Operation Queues):
    • 這裏的隊列指操做隊列,即用來    存放操做的隊列。不一樣於 GCD 中的調度隊列 FIFO(先進先出)的原則。NSOperationQueue 對於添加到隊列中的操做,首先進入準備就緒的狀態(就緒狀態取決於操做之間的依賴關係),而後進入就緒狀態的操做的開始執行順序(非結束執行順序)由操做之間相對的優先級決定(優先級是操做對象自身的屬性)。
    • 操做隊列經過設置 最大併發操做數(maxConcurrentOperationCount) 來控制併發、串行。
    • NSOperationQueue 爲咱們提供了兩種不一樣類型的隊列:主隊列和自定義隊列主隊列運行在主線程之上,而自定義隊列在後臺執行

  • 使用步驟

    由於默認狀況下,NSOperation 單獨使用時系統同步執行操做,
    配合 NSOperationQueue 咱們能更好的實現異步執行。
    bash

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

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

    以後呢,
    系統就會自動將 NSOperationQueue 中的 NSOperation 取出來,在新線程中執行操做。併發


    基本使用

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

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

    在不使用 NSOperationQueue,單獨使用 NSOperation 的狀況下系統同步執行操做,下面咱們學習如下操做的三種建立方式。異步



    1. 使用子類 NSInvocationOperation學習

    // 1.建立 NSInvocationOperation 對象
    NSInvocationOperation *op = [[NSInvocationOperation alloc]     initWithTarget:self selector:@selector(task1) object:nil];
    
        // 2.調用 start 方法開始執行操做
        [op start];
    }
    複製代碼


    在沒有使用 NSOperationQueue、在主線程中單獨使用使用子類 NSInvocationOperation 執行一個操做的狀況下,操做是在當前線程執行的,並無開啓新線程。ui

    若是在其餘線程中執行操做,則打印結果爲其餘線程。

    atom

    2. 使用子類 NSBlockOperation

    // 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 對象封裝了多個操做。NSBlockOperation 是否開啓新線程,取決於操做的個數。若是添加的操做的個數多,就會自動開啓新線程。固然開啓的線程數是由系統來決定的。


    3. 使用自定義繼承自 NSOperation 的子類

    能夠經過重寫 main 或者 start 方法 來定義本身的 NSOperation 對象。
    重寫main方法比較簡單,咱們不須要管理操做的狀態屬性 isExecutingisFinished
    main 執行完返回的時候,這個操做就結束了。

    • 能夠看出:在沒有使用 NSOperationQueue、在主線程單獨使用自定義繼承自 NSOperation 的子類的狀況下,是在主線程執行操做,並無開啓新線程。



    建立隊列 

    NSOperationQueue 一共有兩種隊列:主隊列、自定義隊列。

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

        // 主隊列獲取方法
        NSOperationQueue *queue = [NSOperationQueue mainQueue]; 複製代碼
    • 自定義隊列(非主隊列)
      • 添加到這種隊列中的操做,就會自動放到子線程中執行。
      • 同時包含了:串行、併發功能。

        // 自定義隊列建立方法
        NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 複製代碼


    將操做加入到隊列中

    總共有兩種方法:

    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: 將操做加入到操做隊列後可以開啓新線程,進行併發執行。


    2. - (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: 將操做加入到操做隊列後可以開啓新線程,進行併發執行。



    NSOperationQueue 控制串行執行、併發執行

    1, 關鍵屬性:  maxConcurrentOperationCount,叫作最大併發操做數

    用來控制一個特定隊列中能夠有多少個操做同時參與併發執行。

    注意:這裏 maxConcurrentOperationCount 控制的 不是併發線程的數量而是一個隊列中同時能併發執行的最大操做數。並且一個操做也並不是只能在一個線程中運行。

    最大併發操做數:maxConcurrentOperationCount

      默認狀況下爲-1,表示不進行限制,可進行併發執行

      爲1時,隊列爲串行隊列。只能串行執行。

      大於1時,隊列爲併發隊列。操做併發執行,固然這個值不該超過系統限制,即便本身設置一個很大的值,系統也會自動調整爲 min{本身設定的值,系統設定的默認最大值}。


    • 能夠看出:當最大併發操做數爲1時,操做是按順序串行執行的,而且一個操做完成以後,下一個操做纔開始執行。

      當最大操做併發數爲2時,操做是併發執行的,能夠同時執行兩個操做。而開啓線程數量是由系統決定的,不須要咱們來管理。

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


    NSOperation 操做依賴

    最吸引人的地方是它能添加操做之間的依賴關係。
    經過操做依賴,咱們能夠很方便的控制操做之間執行前後順序。
    NSOperation 提供了3個接口供咱們管理和查看依賴。

    • - (void)addDependency:(NSOperation *)op; 添加依賴,使當前操做依賴於操做 op 的完成。
    • - (void)removeDependency:(NSOperation *)op; 移除依賴,取消當前操做對操做 op 的依賴。
    • @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在當前操做開始執行以前完成執行的全部操做對象數組。

    好比說有 A、B 兩個操做,其中 A 執行完操做,B 才能執行操做。

    若是使用依賴來處理的話,那麼就須要讓操做 B 依賴於操做 A。


    NSOperation 優先級

    queuePriority(優先級)屬性,queuePriority屬性適用於同一操做隊列中的操做,不適用於不一樣操做隊列中的操做。

    默認狀況下,全部新建立的操做對象優先級都是NSOperationQueuePriorityNormal

    可是咱們能夠經過setQueuePriority:方法來改變當前操做在同一隊列中的執行優先級。

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

    對於添加到隊列中的操做,
    首先進入準備就緒的狀態(就緒狀態取決於操做之間的依賴關係)
    而後進入就緒狀態的操做的 開始執行順序(非結束執行順序)由操做之間相對的優先級決定(優先級是操做對象自身的屬性)


    一串默認Normal優先級的操做 ,沒有須要依賴的操做 會先進入準備就緒狀態。

    優先級  屬性決定了進入準備就緒狀態下的操做之間的開始執行順序。
    而且,優先級不能取代依賴關係。

    依賴關係 >   優先級
    優先級不能取代依賴關係。若是要控制操做間的啓動順序,則必須使用依賴關係

    NSOperation、NSOperationQueue 線程間的通訊

    通常在主線程裏邊進行 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]); // 打印當前線程
                }
            }];
        }];
    }複製代碼

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

    • 線程安全:若是你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。
      若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。
       若每一個線程中對全局變量、靜態變量只有讀操做,而無寫操做,通常來講,這個全局變量是線程安全的;
      如有多個線程同時執行寫操做(更改變量),通常都須要考慮線程同步,不然的話就可能影響線程安全。
    • 線程同步:可理解爲線程 A 和 線程 B 一塊配合,A 執行到必定程度時要依靠線程 B 的某個結果,因而停下來,示意 B 運行;B 依言執行,再將結果給 A;A 再繼續操做。


    實例「模擬火車票售賣」

    下面,咱們模擬火車票售賣的方式,實現 NSOperation 線程安全和解決線程同步問題。 場景:總共有50張火車票,有兩個售賣火車票的窗口,一個是北京火車票售賣窗口,另外一個是上海火車票售賣窗口。兩個窗口同時售賣火車票,賣完爲止。


    /**
     * 非線程安全:不使用 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;
            }
        }
    } 複製代碼


    • 能夠看到:在不考慮線程安全,不使用 NSLock 狀況下,獲得票數是錯亂的,這樣顯然不符合咱們的需求,因此咱們須要考慮線程安全問題。

    線程安全解決方案:能夠給線程加鎖,
    在一個線程執行該操做的時候,不容許其餘線程進行操做。

    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;
            }
        }
    } 複製代碼
    • 能夠看出:在考慮了線程安全,使用 NSLock 加鎖、解鎖機制的狀況下,獲得的票數是正確的,沒有出現混亂的狀況。咱們也就解決了多個線程同步的問題。

    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. 暫停和取消的區別就在於:暫停操做以後還能夠恢復操做,繼續向下執行;而取消操做以後,全部的操做就清空了,沒法再接着執行剩下的操做。
    相關文章
    相關標籤/搜索