iOS多线程详解:实践篇
iOS多線程實踐中,常用的就是子線程執行耗時操作,然后回到主線程刷新UI。在iOS中每個進程啟動后都會建立一個主線程(UI線程),這個線程是其他線程的父線程。由于在iOS中除了主線程,其他子線程是獨立于Cocoa Touch的,所以只有主線程可以更新UI界面。iOS多線程開發實踐方式有4種,分別為Pthreads、NSThread、GCD、NSOperation,下面分別講一講各自的使用方式,以及優缺點。
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方式,先創建線程對象,再啟動
打印結果: 2018-03-16 13:47:25.133244+0800 ocgcd[13811:5776836] 當前線程<NSThread: 0x1c0264480>{number = 4, name = thread1}
- detachNewThreadSelector顯式創建并啟動線程
打印結果: 2018-03-16 13:49:34.620546+0800 ocgcd[13814:5777803] 當前線程<NSThread: 0x1c026a940>{number = 5, name = (null)}
- performSelectorInBackground隱式創建并啟動線程
打印結果: 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線程狀態
- 啟動線程
- 阻塞線程
- 結束線程
關于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)}
線程的狀態如下圖:
1、新建:實例化對象
2、就緒:向線程對象發送start消息,線程對象被加入“可調度線程池”等待CPU調度;detach方法和performSelectorInBackground方法會直接實例化一個線程對象并加入“可調度線程池”
3、運行:CPU負責調度“可調度線程池”中線程的執行,線程執行完成之前,狀態可能會在“就緒”和“運行”之間來回切換,“就緒”和“運行”之間的狀態變化由CPU負責,程序員不能干預
4、阻塞:當滿足某個預定條件時,可以使用休眠或鎖阻塞線程執行,影響的方法有:sleepForTimeInterval,sleepUntilDate,@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}//回到主線程刷新UIself.performSelector(onMainThread: #selector(downloadFinished(_:)), with: img, waitUntilDone: false) }@objc func downloadFinished(_ img: UIImage) {//打印當前線程print("刷新UI線程", Thread.current)// 賦值圖片到imageviewself.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多線程詳解:概念篇中有詳細說明
- 創建串行隊列
使用DispatchQueue初始化創建隊列,默認是串行隊列。第一個參數是表示隊列的唯一標識符,用于 DEBUG,可為空,Dispatch Queue 的名稱推薦使用應用程序 ID 這種逆序全程域名。
- 創建并發隊列
第二個參數輸入.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)//添加任務1que.sync {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作print("任務1Thread---", Thread.current) //打印當前線程}}//添加任務2que.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)//添加任務1que.async {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作print("任務1Thread---", Thread.current) //打印當前線程}}//添加任務2que.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")//添加任務1que.sync {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作print("任務1Thread---", Thread.current) //打印當前線程}}//添加任務2que.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")//添加任務1que.async {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作print("任務1Thread---", Thread.current) //打印當前線程}}//添加任務2que.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//添加任務1que.sync {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作print("任務1Thread---", Thread.current) //打印當前線程}}//添加任務2que.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//添加任務1que.sync {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作print("任務1Thread---", Thread.current) //打印當前線程}}//添加任務2que.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//添加任務1que.async {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2) //模擬耗時操作print("任務1Thread---", Thread.current) //打印當前線程}}//添加任務2que.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")//創建workItemlet workItem = DispatchWorkItem.init {for _ in 0..<2 {print("任務workItem---", Thread.current)}}//全局隊列(并發隊列)執行workItemDispatchQueue.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")//創建 DispatchGrouplet group =DispatchGroup()group.enter()//全局隊列(并發隊列)執行任務DispatchQueue.global().async {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//耗時操作print("任務1------", Thread.current)//打印線程}group.leave()}//如果需要上個隊列完成后再執行可以用waitgroup.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()}//全部執行完后回到主線程刷新UIgroup.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_async是oc的實現,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")//創建并發隊列1let que1 = DispatchQueue.init(label: "com.jackyshan.thread1", qos: .userInteractive, attributes: .concurrent)//創建并發隊列2let 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")//創建并發隊列1let que1 = DispatchQueue.init(label: "com.jackyshan.thread1", qos: .background, attributes: .concurrent)//創建并發隊列2let 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的優先級調低為background,queue2的任務就優先執行了。
還有其他的優先級,從高到低,就不一一相互比較了。
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()}//創建并發隊列1let que1 = DispatchQueue.init(label: "com.jackyshan.thread1", qos: .background, attributes: .concurrent)//創建并發隊列2let 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 = truepublic let timer = DispatchSource.makeTimerSource()timer.setEventHandler {//這里要注意循環引用,[weak self] inprint("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 = 0userData.setEventHandler {let pendingData = userData.dataglobalData = globalData + pendingDataprint("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.mainqueue.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創建任務
- 通過OperationQueue創建任務
NSOperation相關方法
最大并發操作數:maxConcurrentOperationCount
maxConcurrentOperationCount 默認情況下為-1,表示不進行限制,可進行并發執行。 maxConcurrentOperationCount這個值不應超過系統限制(64),即使自己設置一個很大的值,系統也會自動調整為 min{自己設定的值,系統設定的默認最大值}。
- 設置maxConcurrentOperationCount為1,實現串行操作。
打印結果: 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)}
從打印結果可以看到隊列里的任務是按串行執行的。 這是因為隊列里的任務優先級一樣,在只有一個并發隊列數的時候,任務按順序執行。
- 設置maxConcurrentOperationCount為1,實現并發操作。
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,實現了隊列的任務交替執行了。
- 設置maxConcurrentOperationCount為11,實現并發操作。
打印結果: 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")//創建并發隊列1let 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)//創建并發隊列2let 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)} 代碼塊------結束
通過設置隊列的waitUntilFinished為true,可以看到queu1的任務并發執行完了之后,queue2的任務才開始并發執行。 而且所有的執行是在代碼塊------begin和代碼塊------結束之間的。queue1和queue2阻塞了主線程。
操作依賴: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 veryLowcase lowcase normalcase highcase 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 } 獲取主隊列。
總結
以上是生活随笔為你收集整理的iOS多线程详解:实践篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 软件工程需求分析—结对
- 下一篇: 服务器的默认远程端口