iOS多執行緒詳解:實踐篇

jackyshan_發表於2018-03-23

iOS多執行緒詳解:實踐篇

iOS多執行緒實踐中,常用的就是子執行緒執行耗時操作,然後回到主執行緒重新整理UI。在iOS中每個程式啟動後都會建立一個主執行緒(UI執行緒),這個執行緒是其他執行緒的父執行緒。由於在iOS中除了主執行緒,其他子執行緒是獨立於Cocoa Touch的,所以只有主執行緒可以更新UI介面。iOS多執行緒開發實踐方式有4種,分別為Pthreads、NSThread、GCD、NSOperation,下面分別講一講各自的使用方式,以及優缺點。

iOS多執行緒詳解:實踐篇

pthread: 跨平臺,適用於多種作業系統,可移植性強,是一套純C語言的通用API,且執行緒的生命週期需要程式設計師自己管理,使用難度較大,所以在實際開發中通常不使用。 NSThread: 基於OC語言的API,使得其簡單易用,物件導向操作。執行緒的宣告週期由程式設計師管理,在實際開發中偶爾使用。 GCD: 基於C語言的API,充分利用裝置的多核,旨在替換NSThread等執行緒技術。執行緒的生命週期由系統自動管理,在實際開發中經常使用。 NSOperation: 基於OC語言API,底層是GCD,增加了一些更加簡單易用的功能,使用更加物件導向。執行緒生命週期由系統自動管理,在實際開發中經常使用。

Pthreads

引自 維基百科 實現POSIX 執行緒標準的庫常被稱作Pthreads,一般用於Unix-like POSIX 系統,如Linux、 Solaris。但是Microsoft Windows上的實現也存在,例如直接使用Windows API實現的第三方庫pthreads-w32;而利用Windows的SFU/SUA子系統,則可以使用微軟提供的一部分原生POSIX API。

其實,這就是一套在很多作業系統上都通用的多執行緒API,所以移植性很強,基於C封裝的一套執行緒框架,iOS上也是適用的。

Pthreads建立執行緒

- (void)onThread {
    // 1. 建立執行緒: 定義一個pthread_t型別變數
    pthread_t thread;
    // 2. 開啟執行緒: 執行任務
    pthread_create(&thread, NULL, run, NULL);
    // 3. 設定子執行緒的狀態設定為detached,該執行緒執行結束後會自動釋放所有資源
    pthread_detach(thread);
}

void * run(void *param) {
    NSLog(@"%@", [NSThread currentThread]);

    return NULL;
}
複製程式碼

列印結果: 2018-03-16 11:06:12.298115+0800 ocgcd[13744:5710531] <NSThread: 0x1c026f100>{number = 4, name = (null)}

如果出現'pthread_create' is invalid in C99報錯,原因是沒有匯入#import <pthread.h>

——pthread_create(&thread, NULL, run, NULL); 中各項引數含義:——

  • 第一個引數&thread是執行緒物件,指向執行緒識別符號的指標
  • 第二個是執行緒屬性,可賦值NULL
  • 第三個run表示指向函式的指標(run對應函式裡是需要在新執行緒中執行的任務)
  • 第四個是執行函式的引數,可賦值NULL

Pthreads其他相關方法

  • pthread_create():建立一個執行緒
  • pthread_exit():終止當前執行緒
  • pthread_cancel():中斷另外一個執行緒的執行
  • pthread_join():阻塞當前的執行緒,直到另外一個執行緒執行結束
  • pthread_attr_init():初始化執行緒的屬性
  • pthread_attr_setdetachstate():設定脫離狀態的屬性(決定這個執行緒在終止時是否可以被結合)
  • pthread_attr_getdetachstate():獲取脫離狀態的屬性
  • pthread_attr_destroy():刪除執行緒的屬性
  • pthread_kill():向執行緒傳送一個訊號

Pthreads常用函式與功能

  • pthread_t

pthread_t用於表示Thread ID,具體內容根據實現的不同而不同,有可能是一個Structure,因此不能將其看作為整數。

  • pthread_equal

pthread_equal函式用於比較兩個pthread_t是否相等。

int pthread_equal(pthread_t tid1, pthread_t tid2)
複製程式碼
  • pthread_self

pthread_self函式用於獲得本執行緒的thread id。

pthread _t pthread_self(void);
複製程式碼

Pthreads實現互斥鎖

鎖可以被動態或靜態建立,可以用巨集PTHREAD_MUTEX_INITIALIZER來靜態的初始化鎖,採用這種方式比較容易理解,互斥鎖是pthread_mutex_t的結構體,而這個巨集是一個結構常量,如下可以完成靜態的初始化鎖:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;也可以用pthread_mutex_init函式動態的建立,函式原型如:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr)

總共有100張火車票,開啟兩個執行緒,北京和上海兩個視窗同時賣票,賣一張票就減去庫存,使用鎖,保證北京和上海賣票的庫存是一致的。實現如下。

#import "ViewController.h"
#include <pthread.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    [self onThread];
}

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
NSMutableArray *tickets;

- (void)onThread {
    tickets = [NSMutableArray array];
    //生成100張票
    for (int i = 0; i < 100; i++) {
        [tickets addObject:[NSNumber numberWithInt:i]];
    }
    
    //執行緒1 北京賣票視窗
    // 1. 建立執行緒1: 定義一個pthread_t型別變數
    pthread_t thread1;
    // 2. 開啟執行緒1: 執行任務
    pthread_create(&thread1, NULL, run, NULL);
    // 3. 設定子執行緒1的狀態設定為detached,該執行緒執行結束後會自動釋放所有資源
    pthread_detach(thread1);
    
    //執行緒2 上海賣票視窗
    // 1. 建立執行緒2: 定義一個pthread_t型別變數
    pthread_t thread2;
    // 2. 開啟執行緒2: 執行任務
    pthread_create(&thread2, NULL, run, NULL);
    // 3. 設定子執行緒2的狀態設定為detached,該執行緒執行結束後會自動釋放所有資源
    pthread_detach(thread2);

}

void * run(void *param) {
    while (true) {
        //鎖門,執行任務
        pthread_mutex_lock(&mutex);
        if (tickets.count > 0) {
            NSLog(@"剩餘票數%ld, 賣票視窗%@", tickets.count, [NSThread currentThread]);
            [tickets removeLastObject];
            [NSThread sleepForTimeInterval:0.2];
        }
        else {
            NSLog(@"票已經賣完了");

            //開門,讓其他任務可以執行
            pthread_mutex_unlock(&mutex);

            break;
        }
        
        //開門,讓其他任務可以執行
        pthread_mutex_unlock(&mutex);
    }

    return NULL;
}

@end
複製程式碼

列印結果: 2018-03-16 11:47:01.069412+0800 ocgcd[13758:5723862] 剩餘票數100, 賣票視窗<NSThread: 0x1c0667600>{number = 5, name = (null)} 2018-03-16 11:47:01.272654+0800 ocgcd[13758:5723863] 剩餘票數99, 賣票視窗<NSThread: 0x1c0672b40>{number = 6, name = (null)} 2018-03-16 11:47:01.488456+0800 ocgcd[13758:5723862] 剩餘票數98, 賣票視窗<NSThread: 0x1c0667600>{number = 5, name = (null)} 2018-03-16 11:47:01.691334+0800 ocgcd[13758:5723863] 剩餘票數97, 賣票視窗<NSThread: 0x1c0672b40>{number = 6, name = (null)} ................. 2018-03-16 11:47:12.110962+0800 ocgcd[13758:5723862] 剩餘票數46, 賣票視窗<NSThread: 0x1c0667600>{number = 5, name = (null)} 2018-03-16 11:47:12.316060+0800 ocgcd[13758:5723863] 剩餘票數45, 賣票視窗<NSThread: 0x1c0672b40>{number = 6, name = (null)} 2018-03-16 11:47:12.529002+0800 ocgcd[13758:5723862] 剩餘票數44, 賣票視窗<NSThread: 0x1c0667600>{number = 5, name = (null)} 2018-03-16 11:47:12.731459+0800 ocgcd[13758:5723863] 剩餘票數43, 賣票視窗<NSThread: 0x1c0672b40>{number = 6, name = (null)} ................. 2018-03-16 11:47:21.103237+0800 ocgcd[13758:5723862] 剩餘票數2, 賣票視窗<NSThread: 0x1c0667600>{number = 5, name = (null)} 2018-03-16 11:47:21.308605+0800 ocgcd[13758:5723863] 剩餘票數1, 賣票視窗<NSThread: 0x1c0672b40>{number = 6, name = (null)} 2018-03-16 11:47:21.511062+0800 ocgcd[13758:5723862] 票已經賣完了 2018-03-16 11:47:21.511505+0800 ocgcd[13758:5723863] 票已經賣完了

對鎖的操作主要包括加鎖pthread_mutex_lock()、解鎖pthread_mutex_unlock()和測試加鎖pthread_mutex_trylock()三個。 pthread_mutex_trylock()語義與pthread_mutex_lock()類似,不同的是在鎖已經被佔據時返回EBUSY而不是掛起等待。

NSThread

NSThread是物件導向的,封裝程度最小最輕量級的,使用更靈活,但要手動管理執行緒的生命週期、執行緒同步和執行緒加鎖等,開銷較大。 NSThread的基本使用比較簡單,可以動態建立初始化NSThread物件,對其進行設定然後啟動;也可以通過NSThread的靜態方法快速建立並啟動新執行緒;此外NSObject基類物件還提供了隱式快速建立NSThread執行緒的performSelector系列類別擴充套件工具方法;NSThread還提供了一些靜態工具介面來控制當前執行緒以及獲取當前執行緒的一些資訊。

NSThread建立執行緒

NSThread有三種建立方式:

  • initWithTarget方式,先建立執行緒物件,再啟動
- (void)onThread {
    // 建立並啟動
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    // 設定執行緒名
    [thread setName:@"thread1"];
    // 設定優先順序,優先順序從0到1,1最高
    [thread setThreadPriority:0.9];
    // 啟動
    [thread start];
}

- (void)run {
    NSLog(@"當前執行緒%@", [NSThread currentThread]);
}
複製程式碼

列印結果: 2018-03-16 13:47:25.133244+0800 ocgcd[13811:5776836] 當前執行緒<NSThread: 0x1c0264480>{number = 4, name = thread1}

  • detachNewThreadSelector顯式建立並啟動執行緒
- (void)onThread {
    // 使用類方法建立執行緒並自動啟動執行緒
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}

- (void)run {
    NSLog(@"當前執行緒%@", [NSThread currentThread]);
}
複製程式碼

列印結果: 2018-03-16 13:49:34.620546+0800 ocgcd[13814:5777803] 當前執行緒<NSThread: 0x1c026a940>{number = 5, name = (null)}

  • performSelectorInBackground隱式建立並啟動執行緒
- (void)onThread {
    // 使用NSObject的方法隱式建立並自動啟動
    [self performSelectorInBackground:@selector(run) withObject:nil];
}

- (void)run {
    NSLog(@"當前執行緒%@", [NSThread currentThread]);
}
複製程式碼

列印結果: 2018-03-16 13:54:33.451895+0800 ocgcd[13820:5780922] 當前執行緒<NSThread: 0x1c4460280>{number = 4, name = (null)}

NSThread方法

//獲取當前執行緒
 +(NSThread *)currentThread; 
//建立執行緒後自動啟動執行緒
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
//是否是多執行緒
+ (BOOL)isMultiThreaded;
//執行緒字典
- (NSMutableDictionary *)threadDictionary;
//執行緒休眠到什麼時間
+ (void)sleepUntilDate:(NSDate *)date;
//執行緒休眠多久
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//取消執行緒
- (void)cancel;
//啟動執行緒
- (void)start;
//退出執行緒
+ (void)exit;
//執行緒優先順序
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
- (double)threadPriority NS_AVAILABLE(10_6, 4_0);
- (void)setThreadPriority:(double)p NS_AVAILABLE(10_6, 4_0);
//呼叫棧返回地址
+ (NSArray *)callStackReturnAddresses NS_AVAILABLE(10_5, 2_0);
+ (NSArray *)callStackSymbols NS_AVAILABLE(10_6, 4_0);
//設定執行緒名字
- (void)setName:(NSString *)n NS_AVAILABLE(10_5, 2_0);
- (NSString *)name NS_AVAILABLE(10_5, 2_0);
//獲取棧的大小
- (NSUInteger)stackSize NS_AVAILABLE(10_5, 2_0);
- (void)setStackSize:(NSUInteger)s NS_AVAILABLE(10_5, 2_0);
// 獲得主執行緒
+ (NSThread *)mainThread;
//是否是主執行緒
- (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0);
+ (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main
+ (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);
//初始化方法
- (id)init NS_AVAILABLE(10_5, 2_0); // designated initializer
- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0);
//是否正在執行
- (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0);
//是否執行完成
- (BOOL)isFinished NS_AVAILABLE(10_5, 2_0);
//是否取消執行緒
- (BOOL)isCancelled NS_AVAILABLE(10_5, 2_0);
- (void)cancel NS_AVAILABLE(10_5, 2_0);
//執行緒啟動
- (void)start NS_AVAILABLE(10_5, 2_0);
- (void)main NS_AVAILABLE(10_5, 2_0); // thread body method
@end
//多執行緒通知
FOUNDATION_EXPORT NSString * const NSWillBecomeMultiThreadedNotification;
FOUNDATION_EXPORT NSString * const NSDidBecomeSingleThreadedNotification;
FOUNDATION_EXPORT NSString * const NSThreadWillExitNotification;

@interface NSObject (NSThreadPerformAdditions)
//與主執行緒通訊
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
  // equivalent to the first method with kCFRunLoopCommonModes
//與其他子執行緒通訊
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
  // equivalent to the first method with kCFRunLoopCommonModes
//隱式建立並啟動執行緒
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg NS_AVAILABLE(10_5, 2_0);
複製程式碼

NSThread執行緒狀態

  • 啟動執行緒
// 執行緒啟動
- (void)start;
複製程式碼
  • 阻塞執行緒
// 執行緒休眠到某一時刻
+ (void)sleepUntilDate:(NSDate *)date;
// 執行緒休眠多久
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
複製程式碼
  • 結束執行緒
// 結束執行緒
+ (void)exit;
複製程式碼

關於cancel的疑問,當使用cancel方法時,只是改變了執行緒的狀態標識,並不能結束執行緒,所以我們要配合isCancelled方法進行使用。

- (void)onThread {
    // 使用NSObject的方法隱式建立並自動啟動
    [self performSelectorInBackground:@selector(run) withObject:nil];
}

- (void)run {
    NSLog(@"當前執行緒%@", [NSThread currentThread]);
    
    for (int i = 0 ; i < 100; i++) {
        if (i == 20) {
            //取消執行緒
            [[NSThread currentThread] cancel];
            NSLog(@"取消執行緒%@", [NSThread currentThread]);
        }
        
        if ([[NSThread currentThread] isCancelled]) {
            NSLog(@"結束執行緒%@", [NSThread currentThread]);
            //結束執行緒
            [NSThread exit];
            NSLog(@"這行程式碼不會列印的");
        }
        
    }
}
複製程式碼

列印結果: 2018-03-16 14:11:44.423324+0800 ocgcd[13833:5787076] 當前執行緒<NSThread: 0x1c4466840>{number = 4, name = (null)} 2018-03-16 14:11:44.425124+0800 ocgcd[13833:5787076] 取消執行緒<NSThread: 0x1c4466840>{number = 4, name = (null)} 2018-03-16 14:11:44.426391+0800 ocgcd[13833:5787076] 結束執行緒<NSThread: 0x1c4466840>{number = 4, name = (null)}

執行緒的狀態如下圖:

iOS多執行緒詳解:實踐篇

1、新建:例項化物件

2、就緒:向執行緒物件傳送start訊息,執行緒物件被加入“可排程執行緒池”等待CPU排程;detach方法和performSelectorInBackground方法會直接例項化一個執行緒物件並加入“可排程執行緒池”

3、執行:CPU負責排程“可排程執行緒池”中執行緒的執行,執行緒執行完成之前,狀態可能會在“就緒”和“執行”之間來回切換,“就緒”和“執行”之間的狀態變化由CPU負責,程式設計師不能干預

4、阻塞:當滿足某個預定條件時,可以使用休眠或鎖阻塞執行緒執行,影響的方法有:sleepForTimeIntervalsleepUntilDate@synchronized(self)執行緒鎖;執行緒物件進入阻塞狀態後,會被從“可排程執行緒池”中移出,CPU 不再排程

5、死亡

死亡方式:

正常死亡:執行緒執行完畢 非正常死亡:執行緒內死亡--->[NSThread exit]:強行中止後,後續程式碼都不會在執行 執行緒外死亡:[threadObj cancel]--->通知執行緒物件取消,線上程執行方法中需要增加isCancelled判斷,如果isCancelled == YES,直接返回

死亡後執行緒物件的isFinished屬性為YES;如果是傳送cancle訊息,執行緒物件的isCancelled屬性為YES;死亡後stackSize == 0,記憶體空間被釋放

NSThread執行緒間通訊

在開發中,我們經常會在子執行緒進行耗時操作,操作結束後再回到主執行緒去重新整理UI。這就涉及到了子執行緒和主執行緒之間的通訊。看一下官方關於NSThread的執行緒間通訊的方法。

// 在主執行緒上執行操作
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
  // equivalent to the first method with kCFRunLoopCommonModes

// 在指定執行緒上執行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);

// 在當前執行緒上執行操作,呼叫 NSObject 的 performSelector:相關方法
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
複製程式碼

下面通過一個經典的下載圖片DEMO來展示執行緒之間的通訊。具體步驟如下: 1、開啟一個子執行緒,在子執行緒中下載圖片。 2、回到主執行緒重新整理UI,將圖片展示在UIImageView中。

func onThread() {
    let urlStr = "http://tupian.aladd.net/2015/7/2941.jpg"
    self.performSelector(inBackground: #selector(downloadImg(_:)), with: urlStr)
}

@objc func downloadImg(_ urlStr: String) {
    //列印當前執行緒
    print("下載圖片執行緒", Thread.current)
    
    //獲取圖片連結
    guard let url = URL.init(string: urlStr) else {return}
    //下載圖片二進位制資料
    guard let data = try? Data.init(contentsOf: url) else {return}
    //設定圖片
    guard let img = UIImage.init(data: data) else {return}
    
    //回到主執行緒重新整理UI
    self.performSelector(onMainThread: #selector(downloadFinished(_:)), with: img, waitUntilDone: false)
}

@objc func downloadFinished(_ img: UIImage) {
    //列印當前執行緒
    print("重新整理UI執行緒", Thread.current)

	// 賦值圖片到imageview
    self.imageView.image = image
}
複製程式碼

NSThread執行緒安全

執行緒安全,也可以被稱為執行緒同步,主要是解決多執行緒爭搶操作資源的問題,就比如火車票,全國各地多個售票視窗同事去售賣同一列火車票。 怎麼保證,多地售票的票池保持一致,就需要用到多執行緒同步的技術去實現了。

NSThread執行緒安全和GCD、NSOperation執行緒安全都是一樣的,實現方法無非就是加鎖(各種鎖的實現)、訊號量、GCD柵欄等。 具體實現,可以看iOS多執行緒詳解:概念篇執行緒同步段落。

GCD

GCD(Grand Central Dispatch是蘋果為多核並行運算提出的C語言併發技術框架。 GCD會自動利用更多的CPU核心; 會自動管理執行緒的生命週期(建立執行緒,排程任務,銷燬執行緒等); 程式設計師只需要告訴GCD想要如何執行什麼任務,不需要編寫任何執行緒管理程式碼。

GCD底層實現

我們使用的GCD的API是C語言函式,全部包含在LIBdispatch庫中,DispatchQueue通過結構體和連結串列被實現為FIFO的佇列;而FIFO的佇列是由dispatch_async等函式追加的Block來管理的;Block不是直接加入FIFO佇列,而是先加入Dispatch Continuation結構體,然後在加入FIFO佇列,Dispatch Continuation用於記憶Block所屬的Dispatch Group和其他一些資訊(相當於上下文)。 Dispatch Queue可通過dispatch_set_target_queue()設定,可以設定執行該Dispatch Queue處理的Dispatch Queue為目標。該目標可像串珠子一樣,設定多個連線在一起的Dispatch Queue,但是在連線串的最後必須設定Main Dispatch Queue,或各種優先順序的Global Dispatch Queue,或是準備用於Serial Dispatch Queue的Global Dispatch Queue

Global Dispatch Queue的8種優先順序:

.High priority .Default Priority .Low Priority .Background Priority .High Overcommit Priority .Default Overcommit Priority .Low Overcommit Priority .Background Overcommit Priority

附有Overcommit的Global Dispatch Queue使用在Serial Dispatch Queue中,不管系統狀態如何,都會強制生成執行緒的 Dispatch Queue。 這8種Global Dispatch Queue各使用1個pthread_workqueue

  • GCD初始化

GCD初始化時,使用pthread_workqueue_create_np函式生成pthread_workqueue。pthread_workqueue包含在Libc提供的pthreads的API中,他使用bsthread_register和workq_open系統呼叫,在初始化XNU核心的workqueue之後獲取workqueue資訊。

其中XNU有四種workqueue:

WORKQUEUE_HIGH_PRIOQUEUE WORKQUEUE_DEFAULT_PRIOQUEUE WORKQUEUE_LOW_PRIOQUEUE WORKQUEUE_BG_PRIOQUEUE

這四種workqueue與Global Dispatch Queue的執行優先順序相同

  • Dispatch Queue執行block的過程

1、當在Global Dispatch Queue中執行Block時,libdispatch從Global Dispatch Queue自身的FIFO中取出Dispatch Continuation,呼叫pthread_workqueue_additem_np函式,將該Global Dispatch Queue、符合其優先順序的workqueue資訊以及執行Dispatch Continuation的回撥函式等傳遞給pthread_workqueue_additem_np函式的引數。

2、thread_workqueue_additem_np()使用workq_kernreturn系統呼叫,通知workqueue增加應當執行的專案。

3、根據該通知,XUN核心基於系統狀態判斷是否要生成執行緒,如果是Overcommit優先順序的Global Dispatch Queue,workqueue則始終生成執行緒。

4、workqueue的執行緒執行pthread_workqueue(),該函式用libdispatch的回撥函式,在回撥函式中執行執行加入到Dispatch Continuatin的Block。

5、Block執行結束後,進行通知Dispatch Group結束,釋放Dispatch Continuation等處理,開始準備執行加入到Dispatch Continuation中的下一個Block。

GCD使用步驟

GCD 的使用步驟其實很簡單,只有兩步。

1、建立一個佇列(序列佇列或併發佇列) 2、將任務追加到任務的等待佇列中,然後系統就會根據任務型別執行任務(同步執行或非同步執行)

佇列的建立方法/獲取方法

iOS系統預設已經存在兩種佇列,主佇列(序列佇列)和全域性佇列(併發佇列),那我們可以利用GCD提供的介面建立併發和序列佇列。

關於同步、非同步、序列、並行的概念和區別,在iOS多執行緒詳解:概念篇中有詳細說明

  • 建立序列佇列
//建立序列佇列
let que = DispatchQueue.init(label: "com.jacyshan.thread")
複製程式碼

使用DispatchQueue初始化建立佇列,預設是序列佇列。第一個引數是表示佇列的唯一識別符號,用於 DEBUG,可為空,Dispatch Queue 的名稱推薦使用應用程式 ID 這種逆序全程域名。

  • 建立併發佇列
//建立併發佇列
let que = DispatchQueue.init(label: "com.jacyshan.thread", attributes: .concurrent)
複製程式碼

第二個引數輸入.concurrent標示建立的是一個併發佇列

  • 獲取主佇列

主佇列(Main Dispatch Queue)是GCD 提供了的一種特殊的序列佇列 所有放在主佇列中的任務,都會放到主執行緒中執行。

//獲取主佇列
let que = DispatchQueue.main
複製程式碼
  • 獲取全域性佇列

GCD 預設提供了全域性併發佇列(Global Dispatch Queue)。

//獲取全域性佇列
let que = DispatchQueue.global()
複製程式碼
任務的建立方法

GCD 提供了同步執行任務的建立方法sync和非同步執行任務建立方法async。

//同步執行任務建立方法
que.sync {
    print("任務1", Thread.current)
}

//非同步執行任務建立方法
que.async {
    print("任務2", Thread.current)
}
複製程式碼

有兩種佇列(序列佇列/併發佇列),兩種任務執行方式(同步執行/非同步執行),那麼我們就有了四種不同的組合方式。這四種不同的組合方式是:

1、同步執行 + 併發佇列 2、非同步執行 + 併發佇列 3、同步執行 + 序列佇列 4、非同步執行 + 序列佇列

系統還提供了兩種特殊佇列:全域性併發佇列、主佇列。全域性併發佇列可以作為普通併發佇列來使用。但是主佇列因為有點特殊,所以我們就又多了兩種組合方式。這樣就有六種不同的組合方式了。

5、同步執行 + 主佇列 6、非同步執行 + 主佇列

六中組合方式區別通過顯示如下。

區別 併發佇列 序列佇列 主佇列
同步執行 沒有開啟新執行緒,序列執行任務 沒有開啟新執行緒,序列執行任務 主執行緒呼叫:死鎖卡住不執行 其他執行緒呼叫:沒有開啟新執行緒,序列執行任務
非同步執行 有開啟新執行緒,併發執行任務 有開啟新執行緒(1條),序列執行任務 沒有開啟新執行緒,序列執行任務

GCD六種組合實現

同步+併發佇列

在當前執行緒中執行任務,任務按順序執行。

    @IBAction func onThread() {
        //列印當前執行緒
        print("currentThread---", Thread.current)
	    print("程式碼塊------begin")
        
        //建立併發佇列
        let que = DispatchQueue.init(label: "com.jackyshan.thread", attributes: .concurrent)
        
        //新增任務1
        que.sync {
            for _ in 0..<2 {
                Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
                print("任務1Thread---", Thread.current) //列印當前執行緒
            }
        }
        
        //新增任務2
        que.sync {
            for _ in 0..<2 {
                Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
                print("任務2Thread---", Thread.current)
            }
        }
	    print("程式碼塊------end")
    }
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c0078f00>{number = 1, name = main} 程式碼塊------begin 任務1Thread--- <NSThread: 0x1c0078f00>{number = 1, name = main} 任務1Thread--- <NSThread: 0x1c0078f00>{number = 1, name = main} 任務2Thread--- <NSThread: 0x1c0078f00>{number = 1, name = main} 任務2Thread--- <NSThread: 0x1c0078f00>{number = 1, name = main} 程式碼塊------end

可以看到任務是在主執行緒執行的,因為同步並沒有開啟新的執行緒。因為同步會阻塞執行緒,所以當我們的任務操作耗時的時候,我們介面的點選和滑動都是無效的。 因為UI的操作也是在主執行緒,但是任務的耗時已經阻塞了執行緒,UI操作是沒有反應的

非同步+併發佇列

開啟多個執行緒,任務交替執行。

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立併發佇列
    let que = DispatchQueue.init(label: "com.jackyshan.thread", attributes: .concurrent)
    
    //新增任務1
    que.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
            print("任務1Thread---", Thread.current) //列印當前執行緒
        }
    }
    
    //新增任務2
    que.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
            print("任務2Thread---", Thread.current)
        }
    }
    print("程式碼塊------end")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c0062180>{number = 1, name = main} 程式碼塊------begin 程式碼塊------end 任務1Thread--- <NSThread: 0x1c02695c0>{number = 4, name = (null)} 任務2Thread--- <NSThread: 0x1c02694c0>{number = 5, name = (null)} 任務1Thread--- <NSThread: 0x1c02695c0>{number = 4, name = (null)} 任務2Thread--- <NSThread: 0x1c02694c0>{number = 5, name = (null)}

可以看到任務是在多個新的執行緒執行完成的,並沒有在主執行緒執行,因此任務在執行耗時操作的時候,並不會影響UI操作。 非同步可以開啟新的執行緒,併發又可以執行多個執行緒的任務。因為非同步沒有阻塞執行緒,程式碼塊------begin 程式碼塊------end立即執行了,其他執行緒執行完耗時操作之後才列印。

同步+序列佇列

在當前執行緒中執行任務,任務按順序執行。

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立序列佇列,DispatchQueue預設是序列佇列
    let que = DispatchQueue.init(label: "com.jackyshan.thread")
    
    //新增任務1
    que.sync {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
            print("任務1Thread---", Thread.current) //列印當前執行緒
        }
    }
    
    //新增任務2
    que.sync {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
            print("任務2Thread---", Thread.current)
        }
    }
    print("程式碼塊------end")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c40658c0>{number = 1, name = main} 程式碼塊------begin 任務1Thread--- <NSThread: 0x1c40658c0>{number = 1, name = main} 任務1Thread--- <NSThread: 0x1c40658c0>{number = 1, name = main} 任務2Thread--- <NSThread: 0x1c40658c0>{number = 1, name = main} 任務2Thread--- <NSThread: 0x1c40658c0>{number = 1, name = main} 程式碼塊------end

同樣的,序列執行同步任務的時候,也沒有開啟新的執行緒,在主執行緒上執行任務,耗時操作會影響UI操作。

非同步+序列佇列

開啟一個新的執行緒,在新的執行緒中執行任務,任務按順序執行。

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立序列佇列,DispatchQueue預設是序列佇列
    let que = DispatchQueue.init(label: "com.jackyshan.thread")
    
    //新增任務1
    que.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
            print("任務1Thread---", Thread.current) //列印當前執行緒
        }
    }
    
    //新增任務2
    que.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
            print("任務2Thread---", Thread.current)
        }
    }
    print("程式碼塊------end")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c407b700>{number = 1, name = main} 程式碼塊------begin 程式碼塊------end 任務1Thread--- <NSThread: 0x1c0462440>{number = 4, name = (null)} 任務1Thread--- <NSThread: 0x1c0462440>{number = 4, name = (null)} 任務2Thread--- <NSThread: 0x1c0462440>{number = 4, name = (null)} 任務2Thread--- <NSThread: 0x1c0462440>{number = 4, name = (null)}

從列印可以看到只開啟了一個執行緒(序列只會開啟一個執行緒),任務是在新的執行緒按順序執行的。 任務是在程式碼塊------begin 程式碼塊------end後執行的(非同步不會等待任務執行完畢)

下面是講__主佇列__,主佇列是一種特殊的序列佇列,所有任務(非同步同步)都會在主執行緒執行。

同步+主佇列

任務在主執行緒中呼叫會出現死鎖,其他執行緒不會。

  • 在主執行緒執行同步+主佇列

介面卡死,所有操作沒有反應。任務互相等待造成死鎖。

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //獲取主佇列
    let que = DispatchQueue.main
    
    //新增任務1
    que.sync {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
            print("任務1Thread---", Thread.current) //列印當前執行緒
        }
    }
    
    //新增任務2
    que.sync {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
            print("任務2Thread---", Thread.current)
        }
    }
    print("程式碼塊------end")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c00766c0>{number = 1, name = main} 程式碼塊------begin

可以看到程式碼塊------begin執行完,後面就不執行的,卡住不動了,等一會還會崩潰。

感覺死鎖很多文章講的不是很清楚,其實流程就是互相等待,簡單解釋如下:

原因是onThread()這個任務是在主執行緒執行的,任務1被新增到主佇列,要等待佇列onThread()任務執行完才會執行。 然後,任務1是在onThread()這個任務中的,按照FIFO的原則,onThread()先被新增到主佇列,應該先執行完,但是任務1在等待onThread()執行完才會執行。 這樣就造成了死鎖,互相等待對方完成任務。

  • 在其他執行緒執行同步+主佇列

主佇列不會開啟新的執行緒,任務按順序在主執行緒執行

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    
    //開啟新的執行緒執行onThread任務
    performSelector(inBackground: #selector(onThread), with: nil)
}

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //獲取主佇列
    let que = DispatchQueue.main
    
    //新增任務1
    que.sync {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
            print("任務1Thread---", Thread.current) //列印當前執行緒
        }
    }
    
    //新增任務2
    que.sync {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
            print("任務2Thread---", Thread.current)
        }
    }
    print("程式碼塊------end")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c0076a80>{number = 4, name = (null)} 程式碼塊------begin 任務1Thread--- <NSThread: 0x1c406d680>{number = 1, name = main} 任務1Thread--- <NSThread: 0x1c406d680>{number = 1, name = main} 任務2Thread--- <NSThread: 0x1c406d680>{number = 1, name = main} 任務2Thread--- <NSThread: 0x1c406d680>{number = 1, name = main} 程式碼塊------end

onThread任務是在其他執行緒執行的,沒有新增到主佇列,所有也不會等待任務1、2的完成,因此不會死鎖。

這裡有個疑問,有的人會想序列佇列+同步併發佇列+同步為什麼不會死鎖呢,其實如果onThread任務和同步任務在同一個佇列中,而且同步任務是在onThread中執行的,也會造成死鎖。 在一個佇列中,就會出現互相等待的現象,剛好同步又不好開啟新的執行緒,這樣就會死鎖了。

非同步+主佇列

主佇列不會開啟新的執行緒,任務按順序在主執行緒執行

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //獲取主佇列
    let que = DispatchQueue.main
    
    //新增任務1
    que.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
            print("任務1Thread---", Thread.current) //列印當前執行緒
        }
    }
    
    //新增任務2
    que.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作
            print("任務2Thread---", Thread.current)
        }
    }
    print("程式碼塊------end")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c4076500>{number = 1, name = main} 程式碼塊------begin 程式碼塊------end 任務1Thread--- <NSThread: 0x1c4076500>{number = 1, name = main} 任務1Thread--- <NSThread: 0x1c4076500>{number = 1, name = main} 任務2Thread--- <NSThread: 0x1c4076500>{number = 1, name = main} 任務2Thread--- <NSThread: 0x1c4076500>{number = 1, name = main}

可以看到onThread任務執行完了,沒有等待任務1、2的完成(非同步立即執行不等待),所以不會死鎖。 主佇列是序列佇列,任務是按順序一個接一個執行的。

GCD的其他方法

asyncAfter延遲執行

很多時候我們希望延遲執行某個任務,這個時候使用DispatchQueue.main.asyncAfter是很方便的。 這個方法並不是立馬執行的,延遲執行也不是絕對準確,可以看到,他是在延遲時間過後,把任務追加到主佇列,如果主佇列有其他耗時任務,這個延遲任務,相對的也要等待任務完成。

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //主執行緒延遲執行
    let delay = DispatchTime.now() + .seconds(3)
    DispatchQueue.main.asyncAfter(deadline: delay) {
        print("asyncAfter---", Thread.current)
    }
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c407f900>{number = 1, name = main} 程式碼塊------begin asyncAfter--- <NSThread: 0x1c407f900>{number = 1, name = main}

DispatchWorkItem

DispatchWorkItem是一個程式碼塊,它可以在任意一個佇列上被呼叫,因此它裡面的程式碼可以在後臺執行,也可以在主執行緒執行。 它的使用真的很簡單,就是一堆可以直接呼叫的程式碼,而不用像之前一樣每次都寫一個程式碼塊。我們也可以使用它的通知完成回撥任務。

做多執行緒的業務的時候,經常會有需求,當我們在做耗時操作的時候完成的時候發個通知告訴我這個任務做完了。

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立workItem
    let workItem = DispatchWorkItem.init {
        for _ in 0..<2 {
            print("任務workItem---", Thread.current)
        }
    }
    
    //全域性佇列(併發佇列)執行workItem
    DispatchQueue.global().async {
        workItem.perform()
    }
    
    //執行完之後通知
    workItem.notify(queue: DispatchQueue.main) {
        print("任務workItem完成---", Thread.current)
    }
    
    print("程式碼塊------結束")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c4079300>{number = 1, name = main} 程式碼塊------begin 程式碼塊------結束 任務workItem--- <NSThread: 0x1c42705c0>{number = 5, name = (null)} 任務workItem--- <NSThread: 0x1c42705c0>{number = 5, name = (null)} 任務workItem完成--- <NSThread: 0x1c4079300>{number = 1, name = main}

可以看到我們使用全域性佇列非同步執行了workItem,任務執行完,收到了通知。

DispatchGroup佇列組

有些複雜的業務可能會有這個需求,幾個佇列執行任務,然後把這些佇列都放到一個組Group裡,當組裡所有佇列的任務都完成了之後,Group發出通知,回到主佇列完成其他任務。

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立 DispatchGroup
    let group =  DispatchGroup()

    group.enter()
    //全域性佇列(併發佇列)執行任務
    DispatchQueue.global().async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//耗時操作
            print("任務1------", Thread.current)//列印執行緒
        }
        
        group.leave()
    }
    
    //如果需要上個佇列完成後再執行可以用wait
    group.wait()
    group.enter()
    //自定義併發佇列執行任務
    DispatchQueue.init(label: "com.jackyshan.thread").async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//耗時操作
            print("任務2------", Thread.current)//列印執行緒
        }
        
        group.leave()
    }
    
    //全部執行完後回到主執行緒重新整理UI
    group.notify(queue: DispatchQueue.main) {
        print("任務執行完畢------", Thread.current)//列印執行緒
    }
    
    print("程式碼塊------結束")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c0261bc0>{number = 1, name = main} 程式碼塊------begin 任務1------ <NSThread: 0x1c046b900>{number = 5, name = (null)} 任務1------ <NSThread: 0x1c046b900>{number = 5, name = (null)} 程式碼塊------結束 任務2------ <NSThread: 0x1c0476b00>{number = 6, name = (null)} 任務2------ <NSThread: 0x1c0476b00>{number = 6, name = (null)} 任務執行完畢------ <NSThread: 0x1c0261bc0>{number = 1, name = main}

兩個佇列,一個執行預設的全域性佇列,一個是自己自定義的併發佇列,兩個佇列都完成之後,group得到了通知。 如果把group.wait()註釋掉,我們會看到兩個佇列的任務會交替執行。

dispatch_barrier_async柵欄方法

dispatch_barrier_asyncoc的實現,Swift的實現que.async(flags: .barrier)這樣。

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立併發佇列
    let que = DispatchQueue.init(label: "com.jackyshan.thread", attributes: .concurrent)

	//併發非同步執行任務
    que.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//耗時操作
            print("任務0------", Thread.current)//列印執行緒
        }
    }

    //併發非同步執行任務
    que.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//耗時操作
            print("任務1------", Thread.current)//列印執行緒
        }
    }
    
    //柵欄方法:等待佇列裡前面的任務執行完之後執行
    que.async(flags: .barrier) {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//耗時操作
            print("任務2------", Thread.current)//列印執行緒
        }
        //執行完之後執行佇列後面的任務
    }
    
    //併發非同步執行任務
    que.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//耗時操作
            print("任務3------", Thread.current)//列印執行緒
        }
    }
    
    print("程式碼塊------結束")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c4078a00>{number = 1, name = main} 程式碼塊------begin 程式碼塊------結束 任務0------ <NSThread: 0x1c427d5c0>{number = 5, name = (null)} 任務1------ <NSThread: 0x1c0470f80>{number = 4, name = (null)} 任務0------ <NSThread: 0x1c427d5c0>{number = 5, name = (null)} 任務1------ <NSThread: 0x1c0470f80>{number = 4, name = (null)} 任務2------ <NSThread: 0x1c0470f80>{number = 4, name = (null)} 任務2------ <NSThread: 0x1c0470f80>{number = 4, name = (null)} 任務3------ <NSThread: 0x1c0470f80>{number = 4, name = (null)} 任務3------ <NSThread: 0x1c0470f80>{number = 4, name = (null)}

可以看到由於任務2執行的barrier的操作,任務0和1交替執行,任務2等待0和1執行完才執行,任務3也是等待任務2執行完畢。 也可以看到由於barrier的操作,並沒有開啟新的執行緒去跑任務。

Quality Of Service(QoS)和優先順序

在使用 GCD 與 dispatch queue 時,我們經常需要告訴系統,應用程式中的哪些任務比較重要,需要更高的優先順序去執行。當然,由於主佇列總是用來處理 UI 以及介面的響應,所以在主執行緒執行的任務永遠都有最高的優先順序。不管在哪種情況下,只要告訴系統必要的資訊,iOS 就會根據你的需求安排好佇列的優先順序以及它們所需要的資源(比如說所需的 CPU 執行時間)。雖然所有的任務最終都會完成,但是,重要的區別在於哪些任務更快完成,哪些任務完成得更晚。

用於指定任務重要程度以及優先順序的資訊,在 GCD 中被稱為 Quality of Service(QoS)。事實上,QoS 是有幾個特定值的列舉型別,我們可以根據需要的優先順序,使用合適的 QoS 值來初始化佇列。如果沒有指定 QoS,則佇列會使用預設優先順序進行初始化。要詳細瞭解 QoS 可用的值,可以參考這個文件,請確保你仔細看過這個文件。下面的列表總結了 Qos 可用的值,它們也被稱為 QoS classes。第一個 class 程式碼了最高的優先順序,最後一個代表了最低的優先順序:

  • userInteractive
  • userInitiated
  • default
  • utility
  • background
  • unspecified

建立兩個佇列,優先順序都是userInteractive,看看效果:

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立併發佇列1
    let que1 = DispatchQueue.init(label: "com.jackyshan.thread1", qos: .userInteractive, attributes: .concurrent)

    //建立併發佇列2
    let que2 = DispatchQueue.init(label: "com.jackyshan.thread2", qos: .userInteractive, attributes: .concurrent)
    
    que1.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//耗時操作
            print("任務1------", Thread.current)//列印執行緒
        }
    }
    
    que2.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//耗時操作
            print("任務2------", Thread.current)//列印執行緒
        }
    }

    print("程式碼塊------結束")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c0073680>{number = 1, name = main} 程式碼塊------begin 程式碼塊------結束 任務1------ <NSThread: 0x1c047cd80>{number = 5, name = (null)} 任務2------ <NSThread: 0x1c0476c40>{number = 3, name = (null)} 任務1------ <NSThread: 0x1c047cd80>{number = 5, name = (null)} 任務2------ <NSThread: 0x1c0476c40>{number = 3, name = (null)}

兩個佇列的優先順序一樣,任務也是交替執行,這和我們預測的一樣。

下面把queue1的優先順序改為background,看看效果:

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立併發佇列1
    let que1 = DispatchQueue.init(label: "com.jackyshan.thread1", qos: .background, attributes: .concurrent)

    //建立併發佇列2
    let que2 = DispatchQueue.init(label: "com.jackyshan.thread2", qos: .userInteractive, attributes: .concurrent)
    
    que1.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//耗時操作
            print("任務1------", Thread.current)//列印執行緒
        }
    }
    
    que2.async {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//耗時操作
            print("任務2------", Thread.current)//列印執行緒
        }
    }

    print("程式碼塊------結束")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c006afc0>{number = 1, name = main} 程式碼塊------begin 程式碼塊------結束 任務2------ <NSThread: 0x1c4070180>{number = 5, name = (null)} 任務1------ <NSThread: 0x1c006d400>{number = 6, name = (null)} 任務2------ <NSThread: 0x1c4070180>{number = 5, name = (null)} 任務1------ <NSThread: 0x1c006d400>{number = 6, name = (null)}

可以看到queue1的優先順序調低為backgroundqueue2的任務就優先執行了。

還有其他的優先順序,從高到低,就不一一相互比較了。

DispatchSemaphore訊號量

GCD 中的訊號量是指 Dispatch Semaphore,是持有計數的訊號。類似於過高速路收費站的欄杆。可以通過時,開啟欄杆,不可以通過時,關閉欄杆。在 Dispatch Semaphore 中,使用計數來完成這個功能,計數為0時等待,不可通過。計數為1或大於1時,計數減1且不等待,可通過。

  • DispatchSemaphore(value: ):用於建立訊號量,可以指定初始化訊號量計數值,這裡我們預設1.
  • semaphore.wait():會判斷訊號量,如果為1,則往下執行。如果是0,則等待。
  • semaphore.signal():代表執行結束,訊號量加1,有等待的任務這個時候才會繼續執行。

可以使用DispatchSemaphore實現執行緒同步,保證執行緒安全。

加入有一個票池,同時幾個執行緒去賣票,我們要保證每個執行緒獲取的票池是一致的。 使用DispatchSemaphore和剛才講的DispatchWorkItem來實現,我們看看效果。

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立票池
    var tickets = [Int]()
    for i in 0..<38 {
        tickets.append(i)
    }
    
    //建立一個初始計數值為1的訊號
    let semaphore = DispatchSemaphore(value: 1)
    
    let workItem = DispatchWorkItem.init {
        semaphore.wait()
        
        if tickets.count > 0 {
            Thread.sleep(forTimeInterval: 0.2)//耗時操作
            print("剩餘票數", tickets.count, Thread.current)
            
            tickets.removeLast()//去票池庫存
        }
        else {
            print("票池沒票了")
        }
        
        semaphore.signal()
    }

    //建立併發佇列1
    let que1 = DispatchQueue.init(label: "com.jackyshan.thread1", qos: .background, attributes: .concurrent)

    //建立併發佇列2
    let que2 = DispatchQueue.init(label: "com.jackyshan.thread2", qos: .userInteractive, attributes: .concurrent)
    
    que1.async {
        for _ in 0..<20 {
            workItem.perform()
        }
    }
    
    que2.async {
        for _ in 0..<20 {
            workItem.perform()
        }
    }

    print("程式碼塊------結束")
}
複製程式碼

currentThread--- <NSThread: 0x1c407a6c0>{number = 1, name = main} 程式碼塊------begin 程式碼塊------結束 剩餘票數 38 <NSThread: 0x1c44706c0>{number = 8, name = (null)} 剩餘票數 37 <NSThread: 0x1c0264c80>{number = 9, name = (null)} 剩餘票數 36 <NSThread: 0x1c44706c0>{number = 8, name = (null)} ................ 剩餘票數 19 <NSThread: 0x1c0264c80>{number = 9, name = (null)} 剩餘票數 18 <NSThread: 0x1c44706c0>{number = 8, name = (null)} 剩餘票數 17 <NSThread: 0x1c0264c80>{number = 9, name = (null)} 剩餘票數 16 <NSThread: 0x1c44706c0>{number = 8, name = (null)} ................ 剩餘票數 2 <NSThread: 0x1c44706c0>{number = 8, name = (null)} 剩餘票數 1 <NSThread: 0x1c0264c80>{number = 9, name = (null)} 票池沒票了 票池沒票了

可以看到我們的資源沒有因為造成資源爭搶而出現資料紊亂。訊號量很好的實現了多執行緒同步的功能。

DispatchSource

DispatchSource provides an interface for monitoring low-level system objects such as Mach ports, Unix descriptors, Unix signals, and VFS nodes for activity and submitting event handlers to dispatch queues for asynchronous processing when such activity occurs. DispatchSource提供了一組介面,用來提交hander監測底層的事件,這些事件包括Mach ports,Unix descriptors,Unix signals,VFS nodes。

Tips: DispatchSource這個class很好的體現了Swift是一門面向協議的語言。這個類是一個工廠類,用來實現各種source。比如DispatchSourceTimer(本身是個協議)表示一個定時器。

  • DispatchSourceProtocol

基礎協議,所有的用到的DispatchSource都實現了這個協議。這個協議的提供了公共的方法和屬性: 由於不同的source是用到的屬性和方法不一樣,這裡只列出幾個公共的方法

  • activate //啟用
  • suspend //掛起
  • resume //繼續
  • cancel //取消(非同步的取消,會保證當前eventHander執行完)
  • setEventHandler //事件處理邏輯
  • setCancelHandler //取消時候的清理邏輯
  • DispatchSourceTimer

在Swift 3中,可以方便的用GCD建立一個Timer(新特性)。DispatchSourceTimer本身是一個協議。 比如,寫一個timer,1秒後執行,然後10秒後自動取消,允許10毫秒的誤差

PlaygroundPage.current.needsIndefiniteExecution = true

public let timer = DispatchSource.makeTimerSource()

timer.setEventHandler {
    //這裡要注意迴圈引用,[weak self] in
    print("Timer fired at \(NSDate())")
}

timer.setCancelHandler {
    print("Timer canceled at \(NSDate())" )
}

timer.scheduleRepeating(deadline: .now() + .seconds(1), interval: 2.0, leeway: .microseconds(10))

print("Timer resume at \(NSDate())")

timer.resume()

DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10), execute:{
    timer.cancel()
})
複製程式碼

deadline表示開始時間,leeway表示能夠容忍的誤差。

DispatchSourceTimer也支援只呼叫一次。

func scheduleOneshot(deadline: DispatchTime, leeway: DispatchTimeInterval = default)
複製程式碼
  • UserData

DispatchSource中UserData部分也是強有力的工具,這部分包括兩個協議,兩個協議都是用來合併資料的變化,只不過一個是按照+(加)的方式,一個是按照|(位與)的方式。

DispatchSourceUserDataAdd DispatchSourceUserDataOr

在使用這兩種Source的時候,GCD會幫助我們自動的將這些改變合併,然後在適當的時候(target queue空閒)的時候,去回撥EventHandler,從而避免了頻繁的回撥導致CPU佔用過多。

let userData = DispatchSource.makeUserDataAddSource()

var globalData:UInt = 0

userData.setEventHandler {
    let pendingData = userData.data
    globalData = globalData + pendingData
    print("Add \(pendingData) to global and current global is \(globalData)")
}

userData.resume()

let serialQueue = DispatchQueue(label: "com")

serialQueue.async {
    for var index in 1...1000 {
        userData.add(data: 1)
    }

    for var index in 1...1000 {
        userData.add(data: 1)
    }
}
複製程式碼

Add 32 to global and current global is 32 Add 1321 to global and current global is 1353 Add 617 to global and current global is 1970 Add 30 to global and current global is 2000

NSOperation

NSOperation是基於GCD的一個抽象基類,將執行緒封裝成要執行的操作,不需要管理執行緒的生命週期和同步,但比GCD可控性更強,例如可以加入操作依賴(addDependency)、設定操作佇列最大可併發執行的操作個數(setMaxConcurrentOperationCount)、取消操作(cancel)等。NSOperation作為抽象基類不具備封裝我們的操作的功能,需要使用兩個它的實體子類:NSBlockOperation和繼承NSOperation自定義子類。NSOperation需要配合NSOperationQueue來實現多執行緒。

NSOperation使用步驟

自定義Operation

繼承Operation建立一個類,並重寫main方法。當呼叫start的時候,會在適當的時候執行main裡面的任務。

class ViewController: UIViewController {
    
    @IBAction func onThread() {
        //列印當前執行緒
        print("currentThread---", Thread.current)
        print("程式碼塊------begin")
        
        let op = JKOperation.init()
        op.start()
        
        print("程式碼塊------結束")
    }
}


class JKOperation: Operation {
    override func main() {
        Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
        print("任務---", Thread.current)
    }
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c007a280>{number = 1, name = main} 程式碼塊------begin 任務--- <NSThread: 0x1c007a280>{number = 1, name = main} 程式碼塊------結束

可以看到自定義JKOperation,初始化之後,呼叫start方法,main方法裡面的任務執行了,是在主執行緒執行的。 因為我們沒有使用OperationQueue,所以沒有建立新的執行緒。

使用BlockOperation

初始化BlockOperation之後,呼叫start方法。

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    let bop = BlockOperation.init {
        Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
        print("任務---", Thread.current)
    }
    bop.start()
    
    print("程式碼塊------結束")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c4070640>{number = 1, name = main} 程式碼塊------begin 任務--- <NSThread: 0x1c4070640>{number = 1, name = main} 程式碼塊------結束

配合OperationQueue實現

初始化OperationQueue之後,呼叫addOperation,程式碼塊就會自定執行,呼叫機制執行是有OperationQueue裡面自動實現的。 addOperation的方法裡面其實是生成了一個BlockOperation物件,然後執行了這個物件的start方法。

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    OperationQueue.init().addOperation {
        Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
        print("任務---", Thread.current)
    }
    
    print("程式碼塊------結束")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c006a700>{number = 1, name = main} 程式碼塊------begin 程式碼塊------結束 任務--- <NSThread: 0x1c0469900>{number = 5, name = (null)}

可以看到OperationQueue初始化,預設是生成了一個併發佇列,而且執行的是一個非同步操作,所以列印任務的執行緒不是在主執行緒。

佇列的建立方法/獲取方法

OperationQueue沒有實現序列佇列的方法,也沒有像GCD那樣實現了一個全域性佇列。 只有併發佇列的實現和主佇列的獲取。

  • 建立併發佇列

併發佇列的任務是併發(幾乎同時)執行的,可以最大發揮CPU多核的優勢。 看到有的說通過maxConcurrentOperationCount設定併發數量1就實現了序列。 實際上是不對的,通過設定優先順序可以控制佇列的任務交替執行,在下面講到maxConcurrentOperationCount會實現程式碼。

//建立併發佇列
let queue = OperationQueue.init()
複製程式碼

OperationQueue初始化,預設實現的是併發佇列。

  • 獲取主佇列

我們的主佇列是序列佇列,任務是一個接一個執行的。

//獲取主佇列
let queue = OperationQueue.main
複製程式碼

獲取主佇列的任務是非同步執行的。

@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //獲取主佇列
    let queue = OperationQueue.main
    
    queue.addOperation {
        Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
        print("任務1---", Thread.current)
    }

    queue.addOperation {
        Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
        print("任務2---", Thread.current)
    }
    
    print("程式碼塊------結束")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c0064f80>{number = 1, name = main} 程式碼塊------begin 程式碼塊------結束 任務1--- <NSThread: 0x1c0064f80>{number = 1, name = main} 任務2--- <NSThread: 0x1c0064f80>{number = 1, name = main}

任務的建立方法
  • 通過BlockOperation建立任務
BlockOperation.init {
    Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
    print("任務1---", Thread.current)
}.start()
複製程式碼
  • 通過OperationQueue建立任務
//建立併發佇列
let queue = OperationQueue.init()

queue.addOperation {
    Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
    print("任務---", Thread.current)
}
複製程式碼

NSOperation相關方法

最大併發運算元:maxConcurrentOperationCount

maxConcurrentOperationCount 預設情況下為-1,表示不進行限制,可進行併發執行。 maxConcurrentOperationCount這個值不應超過系統限制(64),即使自己設定一個很大的值,系統也會自動調整為 min{自己設定的值,系統設定的預設最大值}。

  • 設定maxConcurrentOperationCount1,實現序列操作。
@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立併發佇列
    let queue = OperationQueue.init()
    //設定最大併發數為1
    queue.maxConcurrentOperationCount = 1

    let bq1 = BlockOperation.init {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
            print("任務1---", Thread.current)
        }
    }
    
    let bq2 = BlockOperation.init {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
            print("任務2---", Thread.current)
        }
    }

    queue.addOperations([bq1, bq2], waitUntilFinished: false)
    
    print("程式碼塊------結束")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c0067880>{number = 1, name = main} 程式碼塊------begin 程式碼塊------結束 任務1--- <NSThread: 0x1c046ad40>{number = 4, name = (null)} 任務1--- <NSThread: 0x1c046ad40>{number = 4, name = (null)} 任務2--- <NSThread: 0x1c046ad40>{number = 4, name = (null)} 任務2--- <NSThread: 0x1c046ad40>{number = 4, name = (null)}

從列印結果可以看到佇列裡的任務是按序列執行的。 這是因為佇列裡的任務優先順序一樣,在只有一個併發佇列數的時候,任務按順序執行。

  • 設定maxConcurrentOperationCount1,實現併發操作。
@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立併發佇列
    let queue = OperationQueue.init()
    //設定最大併發數為1
    queue.maxConcurrentOperationCount = 1

    let bq1 = BlockOperation.init {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
            print("任務1---", Thread.current)
        }
    }
    bq1.queuePriority = .low
    
    let bq2 = BlockOperation.init {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
            print("任務2---", Thread.current)
        }
    }
    bq2.queuePriority = .high
    
    let bq3 = BlockOperation.init {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
            print("任務3---", Thread.current)
        }
    }
    bq3.queuePriority = .normal

    queue.addOperations([bq1, bq2, bq3], waitUntilFinished: false)
    
    print("程式碼塊------結束")
}
複製程式碼

currentThread--- <NSThread: 0x1c4261780>{number = 1, name = main} 程式碼塊------begin 程式碼塊------結束 任務2--- <NSThread: 0x1c0279340>{number = 4, name = (null)} 任務2--- <NSThread: 0x1c0279340>{number = 4, name = (null)} 任務3--- <NSThread: 0x1c0279340>{number = 4, name = (null)} 任務3--- <NSThread: 0x1c0279340>{number = 4, name = (null)} 任務1--- <NSThread: 0x1c0279340>{number = 4, name = (null)} 任務1--- <NSThread: 0x1c0279340>{number = 4, name = (null)}

可以我們通過設定優先順序queuePriority,實現了佇列的任務交替執行了。

  • 設定maxConcurrentOperationCount11,實現併發操作。
@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立併發佇列
    let queue = OperationQueue.init()
    //設定最大併發數為1
    queue.maxConcurrentOperationCount = 11

    let bq1 = BlockOperation.init {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
            print("任務1---", Thread.current)
        }
    }
    
    let bq2 = BlockOperation.init {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
            print("任務2---", Thread.current)
        }
    }

    queue.addOperations([bq1, bq2], waitUntilFinished: false)
    
    print("程式碼塊------結束")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c407a200>{number = 1, name = main} 程式碼塊------begin 程式碼塊------結束 任務2--- <NSThread: 0x1c42647c0>{number = 4, name = (null)} 任務1--- <NSThread: 0x1c04714c0>{number = 3, name = (null)} 任務2--- <NSThread: 0x1c42647c0>{number = 4, name = (null)} 任務1--- <NSThread: 0x1c04714c0>{number = 3, name = (null)}

maxConcurrentOperationCount大於1的時候,實現了併發操作。

等待執行完成:waitUntilFinished

waitUntilFinished阻塞當前執行緒,直到該操作結束。可用於執行緒執行順序的同步。

比如實現兩個併發佇列按順序執行。

    @IBAction func onThread() {
        //列印當前執行緒
        print("currentThread---", Thread.current)
        print("程式碼塊------begin")
        
        //建立併發佇列1
        let queue1 = OperationQueue.init()

        let bq1 = BlockOperation.init {
            for _ in 0..<2 {
                Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
                print("任務1---", Thread.current)
            }
        }
        
        let bq2 = BlockOperation.init {
            for _ in 0..<2 {
                Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
                print("任務2---", Thread.current)
            }
        }

        queue1.addOperations([bq1, bq2], waitUntilFinished: true)
        
        //建立併發佇列2
        let queue2 = OperationQueue.init()
        
        let bq3 = BlockOperation.init {
            for _ in 0..<2 {
                Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
                print("任務3---", Thread.current)
            }
        }
        
        let bq4 = BlockOperation.init {
            for _ in 0..<2 {
                Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
                print("任務4---", Thread.current)
            }
        }
        
        queue2.addOperations([bq3, bq4], waitUntilFinished: true)
        
        print("程式碼塊------結束")
    }
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c407d1c0>{number = 1, name = main} 程式碼塊------begin 任務1--- <NSThread: 0x1c0467d40>{number = 4, name = (null)} 任務2--- <NSThread: 0x1c0460a00>{number = 3, name = (null)} 任務1--- <NSThread: 0x1c0467d40>{number = 4, name = (null)} 任務2--- <NSThread: 0x1c0460a00>{number = 3, name = (null)} 任務3--- <NSThread: 0x1c0467d40>{number = 4, name = (null)} 任務4--- <NSThread: 0x1c0460a00>{number = 3, name = (null)} 任務3--- <NSThread: 0x1c0467d40>{number = 4, name = (null)} 任務4--- <NSThread: 0x1c0460a00>{number = 3, name = (null)} 程式碼塊------結束

通過設定佇列的waitUntilFinishedtrue,可以看到queu1的任務併發執行完了之後,queue2的任務才開始併發執行。 而且所有的執行是在程式碼塊------begin程式碼塊------結束之間的。queue1queue2阻塞了主執行緒。

操作依賴:addDependency
@IBAction func onThread() {
    //列印當前執行緒
    print("currentThread---", Thread.current)
    print("程式碼塊------begin")
    
    //建立併發佇列
    let queue = OperationQueue.init()

    let bq1 = BlockOperation.init {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
            print("任務1---", Thread.current)
        }
    }
    
    let bq2 = BlockOperation.init {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
            print("任務2---", Thread.current)
        }
    }
    
    let bq3 = BlockOperation.init {
        for _ in 0..<2 {
            Thread.sleep(forTimeInterval: 0.2)//執行耗時操作
            print("任務3---", Thread.current)
        }
    }
    
    bq3.addDependency(bq1)
    
    queue.addOperations([bq1, bq2, bq3], waitUntilFinished: false)
    
    print("程式碼塊------結束")
}
複製程式碼

列印結果: currentThread--- <NSThread: 0x1c0065740>{number = 1, name = main} 程式碼塊------begin 程式碼塊------結束 任務1--- <NSThread: 0x1c0660300>{number = 5, name = (null)} 任務2--- <NSThread: 0x1c4071340>{number = 6, name = (null)} 任務1--- <NSThread: 0x1c0660300>{number = 5, name = (null)} 任務2--- <NSThread: 0x1c4071340>{number = 6, name = (null)} 任務3--- <NSThread: 0x1c0660300>{number = 5, name = (null)} 任務3--- <NSThread: 0x1c0660300>{number = 5, name = (null)}

不新增操作依賴

列印結果: currentThread--- <NSThread: 0x1c4072c40>{number = 1, name = main} 程式碼塊------begin 程式碼塊------結束 任務1--- <NSThread: 0x1c0270c80>{number = 7, name = (null)} 任務2--- <NSThread: 0x1c447df00>{number = 4, name = (null)} 任務3--- <NSThread: 0x1c0270cc0>{number = 8, name = (null)} 任務1--- <NSThread: 0x1c0270c80>{number = 7, name = (null)} 任務2--- <NSThread: 0x1c447df00>{number = 4, name = (null)} 任務3--- <NSThread: 0x1c0270cc0>{number = 8, name = (null)}

可以看到任務3在新增了操作依賴任務1,執行就一直等待任務1完成。

優先順序:queuePriority

NSOperation 提供了queuePriority(優先順序)屬性,queuePriority屬性適用於同一操作佇列中的操作,不適用於不同操作佇列中的操作。預設情況下,所有新建立的操作物件優先順序都是NSOperationQueuePriorityNormal。但是我們可以通過setQueuePriority:方法來改變當前操作在同一佇列中的執行優先順序。

public enum QueuePriority : Int {

    case veryLow

    case low

    case normal

    case high

    case veryHigh
}
複製程式碼
  • 當一個操作的所有依賴都已經完成時,操作物件通常會進入準備就緒狀態,等待執行。

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

  • 如果一個佇列中既包含高優先順序操作,又包含低優先順序操作,並且兩個操作都已經準備就緒,那麼佇列先執行高優先順序操作。比如上例中,如果 op1 和 op4 是不同優先順序的操作,那麼就會先執行優先順序高的操作。

  • 如果,一個佇列中既包含了準備就緒狀態的操作,又包含了未準備就緒的操作,未準備就緒的操作優先順序比準備就緒的操作優先順序高。那麼,雖然準備就緒的操作優先順序低,也會優先執行。優先順序不能取代依賴關係。如果要控制操作間的啟動順序,則必須使用依賴關係。

NSOperation常用屬性和方法
  • 取消操作方法

open func cancel()可取消操作,實質是標記isCancelled狀態。

  • 判斷操作狀態方法

open var isExecuting: Bool { get }判斷操作是否正在在執行。

open var isFinished: Bool { get }判斷操作是否已經結束。

open var isConcurrent: Bool { get }判斷操作是否處於序列。

open var isAsynchronous: Bool { get }判斷操作是否處於併發。

open var isReady: Bool { get }判斷操作是否處於準備就緒狀態,這個值和操作的依賴關係相關。

open var isCancelled: Bool { get }判斷操作是否已經標記為取消。

  • 操作同步

open func waitUntilFinished()阻塞當前執行緒,直到該操作結束。可用於執行緒執行順序的同步。

open var completionBlock: (() -> Swift.Void)?會在當前操作執行完畢時執行 completionBlock。

open func addDependency(_ op: Operation)新增依賴,使當前操作依賴於操作 op 的完成。

open func removeDependency(_ op: Operation)移除依賴,取消當前操作對操作 op 的依賴。

open var dependencies: [Operation] { get }在當前操作開始執行之前完成執行的所有操作物件陣列。

open var queuePriority: Operation.QueuePriority設定當前操作在佇列中的優先順序。

NSOperationQueue常用屬性和方法
  • 取消/暫停/恢復操作

open func cancelAllOperations()可以取消佇列的所有操作。

open var isSuspended: Bool判斷佇列是否處於暫停狀態。true為暫停狀態,false為恢復狀態。可設定操作的暫停和恢復,true代表暫停佇列,false代表恢復佇列。

  • 操作同步

open func waitUntilAllOperationsAreFinished()阻塞當前執行緒,直到佇列中的操作全部執行完畢。

  • 新增/獲取操作

open func addOperation(_ block: @escaping () -> Swift.Void) 向佇列中新增一個 NSBlockOperation 型別操作物件。

open func addOperations(_ ops: [Operation], waitUntilFinished wait: Bool)向佇列中新增運算元組,wait 標誌是否阻塞當前執行緒直到所有操作結束。

open var operations: [Operation] { get }當前在佇列中的運算元組(某個操作執行結束後會自動從這個陣列清除)。

open var operationCount: Int { get }當前佇列中的運算元。

  • 獲取佇列

open class var current: OperationQueue? { get }獲取當前佇列,如果當前執行緒不是在 NSOperationQueue 上執行則返回 nil。

open class var main: OperationQueue { get } 獲取主佇列。

關注我

歡迎關注公眾號:jackyshan,技術乾貨首發微信,第一時間推送。

iOS多執行緒詳解:實踐篇

相關文章