iOS多线程详解:实践篇
iOS多線程實踐中,常用的就是子線程執(zhí)行耗時操作,然后回到主線程刷新UI。在iOS中每個進程啟動后都會建立一個主線程(UI線程),這個線程是其他線程的父線程。由于在iOS中除了主線程,其他子線程是獨立于Cocoa Touch的,所以只有主線程可以更新UI界面。iOS多線程開發(fā)實踐方式有4種,分別為Pthreads、NSThread、GCD、NSOperation,下面分別講一講各自的使用方式,以及優(yōu)缺點。
pthread: 跨平臺,適用于多種操作系統(tǒng),可移植性強,是一套純C語言的通用API,且線程的生命周期需要程序員自己管理,使用難度較大,所以在實際開發(fā)中通常不使用。 NSThread: 基于OC語言的API,使得其簡單易用,面向?qū)ο蟛僮鳌>€程的聲明周期由程序員管理,在實際開發(fā)中偶爾使用。 GCD: 基于C語言的API,充分利用設備的多核,旨在替換NSThread等線程技術(shù)。線程的生命周期由系統(tǒng)自動管理,在實際開發(fā)中經(jīng)常使用。 NSOperation: 基于OC語言API,底層是GCD,增加了一些更加簡單易用的功能,使用更加面向?qū)ο蟆>€程生命周期由系統(tǒng)自動管理,在實際開發(fā)中經(jīng)常使用。
Pthreads
引自 維基百科 實現(xiàn)POSIX 線程標準的庫常被稱作Pthreads,一般用于Unix-like POSIX 系統(tǒng),如Linux、 Solaris。但是Microsoft Windows上的實現(xiàn)也存在,例如直接使用Windows API實現(xiàn)的第三方庫pthreads-w32;而利用Windows的SFU/SUA子系統(tǒng),則可以使用微軟提供的一部分原生POSIX API。
其實,這就是一套在很多操作系統(tǒng)上都通用的多線程API,所以移植性很強,基于C封裝的一套線程框架,iOS上也是適用的。
Pthreads創(chuàng)建線程
- (void)onThread {// 1. 創(chuàng)建線程: 定義一個pthread_t類型變量pthread_t thread;// 2. 開啟線程: 執(zhí)行任務pthread_create(&thread, NULL, run, NULL);// 3. 設置子線程的狀態(tài)設置為detached,該線程運行結(jié)束后會自動釋放所有資源pthread_detach(thread); }void * run(void *param) {NSLog(@"%@", [NSThread currentThread]);return NULL; }打印結(jié)果: 2018-03-16 11:06:12.298115+0800 ocgcd[13744:5710531] <NSThread: 0x1c026f100>{number = 4, name = (null)}
如果出現(xiàn)'pthread_create' is invalid in C99報錯,原因是沒有導入#import <pthread.h>
——pthread_create(&thread, NULL, run, NULL); 中各項參數(shù)含義:——
- 第一個參數(shù)&thread是線程對象,指向線程標識符的指針
- 第二個是線程屬性,可賦值NULL
- 第三個run表示指向函數(shù)的指針(run對應函數(shù)里是需要在新線程中執(zhí)行的任務)
- 第四個是運行函數(shù)的參數(shù),可賦值NULL
Pthreads其他相關(guān)方法
- pthread_create():創(chuàng)建一個線程
- pthread_exit():終止當前線程
- pthread_cancel():中斷另外一個線程的運行
- pthread_join():阻塞當前的線程,直到另外一個線程運行結(jié)束
- pthread_attr_init():初始化線程的屬性
- pthread_attr_setdetachstate():設置脫離狀態(tài)的屬性(決定這個線程在終止時是否可以被結(jié)合)
- pthread_attr_getdetachstate():獲取脫離狀態(tài)的屬性
- pthread_attr_destroy():刪除線程的屬性
- pthread_kill():向線程發(fā)送一個信號
Pthreads常用函數(shù)與功能
- pthread_t
pthread_t用于表示Thread ID,具體內(nèi)容根據(jù)實現(xiàn)的不同而不同,有可能是一個Structure,因此不能將其看作為整數(shù)。
- pthread_equal
pthread_equal函數(shù)用于比較兩個pthread_t是否相等。
int pthread_equal(pthread_t tid1, pthread_t tid2)- pthread_self
pthread_self函數(shù)用于獲得本線程的thread id。
pthread _t pthread_self(void);Pthreads實現(xiàn)互斥鎖
鎖可以被動態(tài)或靜態(tài)創(chuàng)建,可以用宏P(guān)THREAD_MUTEX_INITIALIZER來靜態(tài)的初始化鎖,采用這種方式比較容易理解,互斥鎖是pthread_mutex_t的結(jié)構(gòu)體,而這個宏是一個結(jié)構(gòu)常量,如下可以完成靜態(tài)的初始化鎖:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;也可以用pthread_mutex_init函數(shù)動態(tài)的創(chuàng)建,函數(shù)原型如:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr)
總共有100張火車票,開啟兩個線程,北京和上海兩個窗口同時賣票,賣一張票就減去庫存,使用鎖,保證北京和上海賣票的庫存是一致的。實現(xiàn)如下。
#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. 創(chuàng)建線程1: 定義一個pthread_t類型變量pthread_t thread1;// 2. 開啟線程1: 執(zhí)行任務pthread_create(&thread1, NULL, run, NULL);// 3. 設置子線程1的狀態(tài)設置為detached,該線程運行結(jié)束后會自動釋放所有資源pthread_detach(thread1);//線程2 上海賣票窗口// 1. 創(chuàng)建線程2: 定義一個pthread_t類型變量pthread_t thread2;// 2. 開啟線程2: 執(zhí)行任務pthread_create(&thread2, NULL, run, NULL);// 3. 設置子線程2的狀態(tài)設置為detached,該線程運行結(jié)束后會自動釋放所有資源pthread_detach(thread2);}void * run(void *param) {while (true) {//鎖門,執(zhí)行任務pthread_mutex_lock(&mutex);if (tickets.count > 0) {NSLog(@"剩余票數(shù)%ld, 賣票窗口%@", tickets.count, [NSThread currentThread]);[tickets removeLastObject];[NSThread sleepForTimeInterval:0.2];}else {NSLog(@"票已經(jīng)賣完了");//開門,讓其他任務可以執(zhí)行pthread_mutex_unlock(&mutex);break;}//開門,讓其他任務可以執(zhí)行pthread_mutex_unlock(&mutex);}return NULL; }@end打印結(jié)果: 2018-03-16 11:47:01.069412+0800 ocgcd[13758:5723862] 剩余票數(shù)100, 賣票窗口<NSThread: 0x1c0667600>{number = 5, name = (null)} 2018-03-16 11:47:01.272654+0800 ocgcd[13758:5723863] 剩余票數(shù)99, 賣票窗口<NSThread: 0x1c0672b40>{number = 6, name = (null)} 2018-03-16 11:47:01.488456+0800 ocgcd[13758:5723862] 剩余票數(shù)98, 賣票窗口<NSThread: 0x1c0667600>{number = 5, name = (null)} 2018-03-16 11:47:01.691334+0800 ocgcd[13758:5723863] 剩余票數(shù)97, 賣票窗口<NSThread: 0x1c0672b40>{number = 6, name = (null)} … 2018-03-16 11:47:12.110962+0800 ocgcd[13758:5723862] 剩余票數(shù)46, 賣票窗口<NSThread: 0x1c0667600>{number = 5, name = (null)} 2018-03-16 11:47:12.316060+0800 ocgcd[13758:5723863] 剩余票數(shù)45, 賣票窗口<NSThread: 0x1c0672b40>{number = 6, name = (null)} 2018-03-16 11:47:12.529002+0800 ocgcd[13758:5723862] 剩余票數(shù)44, 賣票窗口<NSThread: 0x1c0667600>{number = 5, name = (null)} 2018-03-16 11:47:12.731459+0800 ocgcd[13758:5723863] 剩余票數(shù)43, 賣票窗口<NSThread: 0x1c0672b40>{number = 6, name = (null)} … 2018-03-16 11:47:21.103237+0800 ocgcd[13758:5723862] 剩余票數(shù)2, 賣票窗口<NSThread: 0x1c0667600>{number = 5, name = (null)} 2018-03-16 11:47:21.308605+0800 ocgcd[13758:5723863] 剩余票數(shù)1, 賣票窗口<NSThread: 0x1c0672b40>{number = 6, name = (null)} 2018-03-16 11:47:21.511062+0800 ocgcd[13758:5723862] 票已經(jīng)賣完了 2018-03-16 11:47:21.511505+0800 ocgcd[13758:5723863] 票已經(jīng)賣完了
對鎖的操作主要包括加鎖pthread_mutex_lock()、解鎖pthread_mutex_unlock()和測試加鎖pthread_mutex_trylock()三個。 pthread_mutex_trylock()語義與pthread_mutex_lock()類似,不同的是在鎖已經(jīng)被占據(jù)時返回EBUSY而不是掛起等待。
NSThread
NSThread是面向?qū)ο蟮?#xff0c;封裝程度最小最輕量級的,使用更靈活,但要手動管理線程的生命周期、線程同步和線程加鎖等,開銷較大。 NSThread的基本使用比較簡單,可以動態(tài)創(chuàng)建初始化NSThread對象,對其進行設置然后啟動;也可以通過NSThread的靜態(tài)方法快速創(chuàng)建并啟動新線程;此外NSObject基類對象還提供了隱式快速創(chuàng)建NSThread線程的performSelector系列類別擴展工具方法;NSThread還提供了一些靜態(tài)工具接口來控制當前線程以及獲取當前線程的一些信息。
NSThread創(chuàng)建線程
NSThread有三種創(chuàng)建方式:
- initWithTarget方式,先創(chuàng)建線程對象,再啟動
打印結(jié)果: 2018-03-16 13:47:25.133244+0800 ocgcd[13811:5776836] 當前線程<NSThread: 0x1c0264480>{number = 4, name = thread1}
- detachNewThreadSelector顯式創(chuàng)建并啟動線程
打印結(jié)果: 2018-03-16 13:49:34.620546+0800 ocgcd[13814:5777803] 當前線程<NSThread: 0x1c026a940>{number = 5, name = (null)}
- performSelectorInBackground隱式創(chuàng)建并啟動線程
打印結(jié)果: 2018-03-16 13:54:33.451895+0800 ocgcd[13820:5780922] 當前線程<NSThread: 0x1c4460280>{number = 4, name = (null)}
NSThread方法
//獲取當前線程+(NSThread *)currentThread; //創(chuàng)建線程后自動啟動線程 + (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; //線程優(yōu)先級 + (double)threadPriority; + (BOOL)setThreadPriority:(double)p; - (double)threadPriority NS_AVAILABLE(10_6, 4_0); - (void)setThreadPriority:(double)p NS_AVAILABLE(10_6, 4_0); //調(diào)用棧返回地址 + (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); //是否正在執(zhí)行 - (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0); //是否執(zhí)行完成 - (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 //隱式創(chuàng)建并啟動線程 - (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg NS_AVAILABLE(10_5, 2_0);NSThread線程狀態(tài)
- 啟動線程
- 阻塞線程
- 結(jié)束線程
關(guān)于cancel的疑問,當使用cancel方法時,只是改變了線程的狀態(tài)標識,并不能結(jié)束線程,所以我們要配合isCancelled方法進行使用。
- (void)onThread {// 使用NSObject的方法隱式創(chuàng)建并自動啟動[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(@"結(jié)束線程%@", [NSThread currentThread]);//結(jié)束線程[NSThread exit];NSLog(@"這行代碼不會打印的");}} }打印結(jié)果: 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] 結(jié)束線程<NSThread: 0x1c4466840>{number = 4, name = (null)}
線程的狀態(tài)如下圖:
1、新建:實例化對象
2、就緒:向線程對象發(fā)送start消息,線程對象被加入“可調(diào)度線程池”等待CPU調(diào)度;detach方法和performSelectorInBackground方法會直接實例化一個線程對象并加入“可調(diào)度線程池”
3、運行:CPU負責調(diào)度“可調(diào)度線程池”中線程的執(zhí)行,線程執(zhí)行完成之前,狀態(tài)可能會在“就緒”和“運行”之間來回切換,“就緒”和“運行”之間的狀態(tài)變化由CPU負責,程序員不能干預
4、阻塞:當滿足某個預定條件時,可以使用休眠或鎖阻塞線程執(zhí)行,影響的方法有:sleepForTimeInterval,sleepUntilDate,@synchronized(self)線程鎖;線程對象進入阻塞狀態(tài)后,會被從“可調(diào)度線程池”中移出,CPU 不再調(diào)度
5、死亡
死亡方式:
正常死亡:線程執(zhí)行完畢 非正常死亡:線程內(nèi)死亡—>[NSThread exit]:強行中止后,后續(xù)代碼都不會在執(zhí)行 線程外死亡:[threadObj cancel]—>通知線程對象取消,在線程執(zhí)行方法中需要增加isCancelled判斷,如果isCancelled == YES,直接返回
死亡后線程對象的isFinished屬性為YES;如果是發(fā)送cancle消息,線程對象的isCancelled屬性為YES;死亡后stackSize == 0,內(nèi)存空間被釋放
NSThread線程間通信
在開發(fā)中,我們經(jīng)常會在子線程進行耗時操作,操作結(jié)束后再回到主線程去刷新UI。這就涉及到了子線程和主線程之間的通信。看一下官方關(guān)于NSThread的線程間通信的方法。
// 在主線程上執(zhí)行操作 - (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// 在指定線程上執(zhí)行操作 - (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);// 在當前線程上執(zhí)行操作,調(diào)用 NSObject 的 performSelector:相關(guān)方法 - (id)performSelector:(SEL)aSelector; - (id)performSelector:(SEL)aSelector withObject:(id)object; - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;下面通過一個經(jīng)典的下載圖片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}//下載圖片二進制數(shù)據(jù)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線程安全
線程安全,也可以被稱為線程同步,主要是解決多線程爭搶操作資源的問題,就比如火車票,全國各地多個售票窗口同事去售賣同一列火車票。 怎么保證,多地售票的票池保持一致,就需要用到多線程同步的技術(shù)去實現(xiàn)了。
NSThread線程安全和GCD、NSOperation線程安全都是一樣的,實現(xiàn)方法無非就是加鎖(各種鎖的實現(xiàn))、信號量、GCD柵欄等。 具體實現(xiàn),可以看iOS多線程詳解:概念篇線程同步段落。
GCD
GCD(Grand Central Dispatch是蘋果為多核并行運算提出的C語言并發(fā)技術(shù)框架。 GCD會自動利用更多的CPU內(nèi)核; 會自動管理線程的生命周期(創(chuàng)建線程,調(diào)度任務,銷毀線程等); 程序員只需要告訴GCD想要如何執(zhí)行什么任務,不需要編寫任何線程管理代碼。
GCD底層實現(xiàn)
我們使用的GCD的API是C語言函數(shù),全部包含在LIBdispatch庫中,DispatchQueue通過結(jié)構(gòu)體和鏈表被實現(xiàn)為FIFO的隊列;而FIFO的隊列是由dispatch_async等函數(shù)追加的Block來管理的;Block不是直接加入FIFO隊列,而是先加入Dispatch Continuation結(jié)構(gòu)體,然后在加入FIFO隊列,Dispatch Continuation用于記憶Block所屬的Dispatch Group和其他一些信息(相當于上下文)。 Dispatch Queue可通過dispatch_set_target_queue()設定,可以設定執(zhí)行該Dispatch Queue處理的Dispatch Queue為目標。該目標可像串珠子一樣,設定多個連接在一起的Dispatch Queue,但是在連接串的最后必須設定Main Dispatch Queue,或各種優(yōu)先級的Global Dispatch Queue,或是準備用于Serial Dispatch Queue的Global Dispatch Queue
Global Dispatch Queue的8種優(yōu)先級:
.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中,不管系統(tǒng)狀態(tài)如何,都會強制生成線程的 Dispatch Queue。 這8種Global Dispatch Queue各使用1個pthread_workqueue
- GCD初始化
GCD初始化時,使用pthread_workqueue_create_np函數(shù)生成pthread_workqueue。pthread_workqueue包含在Libc提供的pthreads的API中,他使用bsthread_register和workq_open系統(tǒng)調(diào)用,在初始化XNU內(nèi)核的workqueue之后獲取workqueue信息。
其中XNU有四種workqueue:
WORKQUEUE_HIGH_PRIOQUEUE WORKQUEUE_DEFAULT_PRIOQUEUE WORKQUEUE_LOW_PRIOQUEUE WORKQUEUE_BG_PRIOQUEUE
這四種workqueue與Global Dispatch Queue的執(zhí)行優(yōu)先級相同
- Dispatch Queue執(zhí)行block的過程
1、當在Global Dispatch Queue中執(zhí)行Block時,libdispatch從Global Dispatch Queue自身的FIFO中取出Dispatch Continuation,調(diào)用pthread_workqueue_additem_np函數(shù),將該Global Dispatch Queue、符合其優(yōu)先級的workqueue信息以及執(zhí)行Dispatch Continuation的回調(diào)函數(shù)等傳遞給pthread_workqueue_additem_np函數(shù)的參數(shù)。
2、thread_workqueue_additem_np()使用workq_kernreturn系統(tǒng)調(diào)用,通知workqueue增加應當執(zhí)行的項目。
3、根據(jù)該通知,XUN內(nèi)核基于系統(tǒng)狀態(tài)判斷是否要生成線程,如果是Overcommit優(yōu)先級的Global Dispatch Queue,workqueue則始終生成線程。
4、workqueue的線程執(zhí)行pthread_workqueue(),該函數(shù)用libdispatch的回調(diào)函數(shù),在回調(diào)函數(shù)中執(zhí)行執(zhí)行加入到Dispatch Continuatin的Block。
5、Block執(zhí)行結(jié)束后,進行通知Dispatch Group結(jié)束,釋放Dispatch Continuation等處理,開始準備執(zhí)行加入到Dispatch Continuation中的下一個Block。
GCD使用步驟
GCD 的使用步驟其實很簡單,只有兩步。
1、創(chuàng)建一個隊列(串行隊列或并發(fā)隊列) 2、將任務追加到任務的等待隊列中,然后系統(tǒng)就會根據(jù)任務類型執(zhí)行任務(同步執(zhí)行或異步執(zhí)行)
隊列的創(chuàng)建方法/獲取方法
iOS系統(tǒng)默認已經(jīng)存在兩種隊列,主隊列(串行隊列)和全局隊列(并發(fā)隊列),那我們可以利用GCD提供的接口創(chuàng)建并發(fā)和串行隊列。
關(guān)于同步、異步、串行、并行的概念和區(qū)別,在iOS多線程詳解:概念篇中有詳細說明
- 創(chuàng)建串行隊列
使用DispatchQueue初始化創(chuàng)建隊列,默認是串行隊列。第一個參數(shù)是表示隊列的唯一標識符,用于 DEBUG,可為空,Dispatch Queue 的名稱推薦使用應用程序 ID 這種逆序全程域名。
- 創(chuàng)建并發(fā)隊列
第二個參數(shù)輸入.concurrent標示創(chuàng)建的是一個并發(fā)隊列
- 獲取主隊列
主隊列(Main Dispatch Queue)是GCD 提供了的一種特殊的串行隊列 所有放在主隊列中的任務,都會放到主線程中執(zhí)行。
//獲取主隊列 let que = DispatchQueue.main- 獲取全局隊列
GCD 默認提供了全局并發(fā)隊列(Global Dispatch Queue)。
//獲取全局隊列 let que = DispatchQueue.global()任務的創(chuàng)建方法
GCD 提供了同步執(zhí)行任務的創(chuàng)建方法sync和異步執(zhí)行任務創(chuàng)建方法async。
//同步執(zhí)行任務創(chuàng)建方法 que.sync {print("任務1", Thread.current) }//異步執(zhí)行任務創(chuàng)建方法 que.async {print("任務2", Thread.current) }有兩種隊列(串行隊列/并發(fā)隊列),兩種任務執(zhí)行方式(同步執(zhí)行/異步執(zhí)行),那么我們就有了四種不同的組合方式。這四種不同的組合方式是:
1、同步執(zhí)行 + 并發(fā)隊列 2、異步執(zhí)行 + 并發(fā)隊列 3、同步執(zhí)行 + 串行隊列 4、異步執(zhí)行 + 串行隊列
系統(tǒng)還提供了兩種特殊隊列:全局并發(fā)隊列、主隊列。全局并發(fā)隊列可以作為普通并發(fā)隊列來使用。但是主隊列因為有點特殊,所以我們就又多了兩種組合方式。這樣就有六種不同的組合方式了。
5、同步執(zhí)行 + 主隊列 6、異步執(zhí)行 + 主隊列
六中組合方式區(qū)別通過顯示如下。
| 同步執(zhí)行 | 沒有開啟新線程,串行執(zhí)行任務 | 沒有開啟新線程,串行執(zhí)行任務 | 主線程調(diào)用:死鎖卡住不執(zhí)行 其他線程調(diào)用:沒有開啟新線程,串行執(zhí)行任務 |
| 異步執(zhí)行 | 有開啟新線程,并發(fā)執(zhí)行任務 | 有開啟新線程(1條),串行執(zhí)行任務 | 沒有開啟新線程,串行執(zhí)行任務 |
GCD六種組合實現(xiàn)
同步+并發(fā)隊列
在當前線程中執(zhí)行任務,任務按順序執(zhí)行。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//創(chuàng)建并發(fā)隊列l(wèi)et 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")}打印結(jié)果: 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
可以看到任務是在主線程執(zhí)行的,因為同步并沒有開啟新的線程。因為同步會阻塞線程,所以當我們的任務操作耗時的時候,我們界面的點擊和滑動都是無效的。 因為UI的操作也是在主線程,但是任務的耗時已經(jīng)阻塞了線程,UI操作是沒有反應的。
異步+并發(fā)隊列
開啟多個線程,任務交替執(zhí)行。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//創(chuàng)建并發(fā)隊列l(wèi)et 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") }打印結(jié)果: 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)}
可以看到任務是在多個新的線程執(zhí)行完成的,并沒有在主線程執(zhí)行,因此任務在執(zhí)行耗時操作的時候,并不會影響UI操作。 異步可以開啟新的線程,并發(fā)又可以執(zhí)行多個線程的任務。因為異步?jīng)]有阻塞線程,代碼塊------begin 代碼塊------end立即執(zhí)行了,其他線程執(zhí)行完耗時操作之后才打印。
同步+串行隊列
在當前線程中執(zhí)行任務,任務按順序執(zhí)行。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//創(chuàng)建串行隊列,DispatchQueue默認是串行隊列l(wèi)et 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") }打印結(jié)果: 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
同樣的,串行執(zhí)行同步任務的時候,也沒有開啟新的線程,在主線程上執(zhí)行任務,耗時操作會影響UI操作。
異步+串行隊列
開啟一個新的線程,在新的線程中執(zhí)行任務,任務按順序執(zhí)行。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//創(chuàng)建串行隊列,DispatchQueue默認是串行隊列l(wèi)et 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") }打印結(jié)果: 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)}
從打印可以看到只開啟了一個線程(串行只會開啟一個線程),任務是在新的線程按順序執(zhí)行的。 任務是在代碼塊------begin 代碼塊------end后執(zhí)行的(異步不會等待任務執(zhí)行完畢)
下面是講__主隊列__,主隊列是一種特殊的串行隊列,所有任務(異步同步)都會在主線程執(zhí)行。
同步+主隊列
任務在主線程中調(diào)用會出現(xiàn)死鎖,其他線程不會。
- 在主線程執(zhí)行同步+主隊列
界面卡死,所有操作沒有反應。任務互相等待造成死鎖。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//獲取主隊列l(wèi)et 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") }打印結(jié)果: currentThread— <NSThread: 0x1c00766c0>{number = 1, name = main} 代碼塊------begin
可以看到代碼塊------begin執(zhí)行完,后面就不執(zhí)行的,卡住不動了,等一會還會崩潰。
感覺死鎖很多文章講的不是很清楚,其實流程就是互相等待,簡單解釋如下:
原因是onThread()這個任務是在主線程執(zhí)行的,任務1被添加到主隊列,要等待隊列onThread()任務執(zhí)行完才會執(zhí)行。 然后,任務1是在onThread()這個任務中的,按照FIFO的原則,onThread()先被添加到主隊列,應該先執(zhí)行完,但是任務1在等待onThread()執(zhí)行完才會執(zhí)行。 這樣就造成了死鎖,互相等待對方完成任務。
- 在其他線程執(zhí)行同步+主隊列
主隊列不會開啟新的線程,任務按順序在主線程執(zhí)行
override func viewDidLoad() {super.viewDidLoad()// Do any additional setup after loading the view, typically from a nib.//開啟新的線程執(zhí)行onThread任務performSelector(inBackground: #selector(onThread), with: nil) }@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//獲取主隊列l(wèi)et 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") }打印結(jié)果: 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任務是在其他線程執(zhí)行的,沒有添加到主隊列,所有也不會等待任務1、2的完成,因此不會死鎖。
這里有個疑問,有的人會想串行隊列+同步和并發(fā)隊列+同步為什么不會死鎖呢,其實如果onThread任務和同步任務在同一個隊列中,而且同步任務是在onThread中執(zhí)行的,也會造成死鎖。 在一個隊列中,就會出現(xiàn)互相等待的現(xiàn)象,剛好同步又不好開啟新的線程,這樣就會死鎖了。
異步+主隊列
主隊列不會開啟新的線程,任務按順序在主線程執(zhí)行
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//獲取主隊列l(wèi)et 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") }打印結(jié)果: 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任務執(zhí)行完了,沒有等待任務1、2的完成(異步立即執(zhí)行不等待),所以不會死鎖。 主隊列是串行隊列,任務是按順序一個接一個執(zhí)行的。
GCD的其他方法
asyncAfter延遲執(zhí)行
很多時候我們希望延遲執(zhí)行某個任務,這個時候使用DispatchQueue.main.asyncAfter是很方便的。 這個方法并不是立馬執(zhí)行的,延遲執(zhí)行也不是絕對準確,可以看到,他是在延遲時間過后,把任務追加到主隊列,如果主隊列有其他耗時任務,這個延遲任務,相對的也要等待任務完成。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//主線程延遲執(zhí)行l(wèi)et delay = DispatchTime.now() + .seconds(3)DispatchQueue.main.asyncAfter(deadline: delay) {print("asyncAfter---", Thread.current)} }打印結(jié)果: currentThread— <NSThread: 0x1c407f900>{number = 1, name = main} 代碼塊------begin asyncAfter— <NSThread: 0x1c407f900>{number = 1, name = main}
DispatchWorkItem
DispatchWorkItem是一個代碼塊,它可以在任意一個隊列上被調(diào)用,因此它里面的代碼可以在后臺運行,也可以在主線程運行。 它的使用真的很簡單,就是一堆可以直接調(diào)用的代碼,而不用像之前一樣每次都寫一個代碼塊。我們也可以使用它的通知完成回調(diào)任務。
做多線程的業(yè)務的時候,經(jīng)常會有需求,當我們在做耗時操作的時候完成的時候發(fā)個通知告訴我這個任務做完了。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//創(chuàng)建workItemlet workItem = DispatchWorkItem.init {for _ in 0..<2 {print("任務workItem---", Thread.current)}}//全局隊列(并發(fā)隊列)執(zhí)行workItemDispatchQueue.global().async {workItem.perform()}//執(zhí)行完之后通知workItem.notify(queue: DispatchQueue.main) {print("任務workItem完成---", Thread.current)}print("代碼塊------結(jié)束") }打印結(jié)果: currentThread— <NSThread: 0x1c4079300>{number = 1, name = main} 代碼塊------begin 代碼塊------結(jié)束 任務workItem— <NSThread: 0x1c42705c0>{number = 5, name = (null)} 任務workItem— <NSThread: 0x1c42705c0>{number = 5, name = (null)} 任務workItem完成— <NSThread: 0x1c4079300>{number = 1, name = main}
可以看到我們使用全局隊列異步執(zhí)行了workItem,任務執(zhí)行完,收到了通知。
DispatchGroup隊列組
有些復雜的業(yè)務可能會有這個需求,幾個隊列執(zhí)行任務,然后把這些隊列都放到一個組Group里,當組里所有隊列的任務都完成了之后,Group發(fā)出通知,回到主隊列完成其他任務。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//創(chuàng)建 DispatchGrouplet group =DispatchGroup()group.enter()//全局隊列(并發(fā)隊列)執(zhí)行任務DispatchQueue.global().async {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//耗時操作print("任務1------", Thread.current)//打印線程}group.leave()}//如果需要上個隊列完成后再執(zhí)行可以用waitgroup.wait()group.enter()//自定義并發(fā)隊列執(zhí)行任務DispatchQueue.init(label: "com.jackyshan.thread").async {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//耗時操作print("任務2------", Thread.current)//打印線程}group.leave()}//全部執(zhí)行完后回到主線程刷新UIgroup.notify(queue: DispatchQueue.main) {print("任務執(zhí)行完畢------", Thread.current)//打印線程}print("代碼塊------結(jié)束") }打印結(jié)果: currentThread— <NSThread: 0x1c0261bc0>{number = 1, name = main} 代碼塊------begin 任務1------ <NSThread: 0x1c046b900>{number = 5, name = (null)} 任務1------ <NSThread: 0x1c046b900>{number = 5, name = (null)} 代碼塊------結(jié)束 任務2------ <NSThread: 0x1c0476b00>{number = 6, name = (null)} 任務2------ <NSThread: 0x1c0476b00>{number = 6, name = (null)} 任務執(zhí)行完畢------ <NSThread: 0x1c0261bc0>{number = 1, name = main}
兩個隊列,一個執(zhí)行默認的全局隊列,一個是自己自定義的并發(fā)隊列,兩個隊列都完成之后,group得到了通知。 如果把group.wait()注釋掉,我們會看到兩個隊列的任務會交替執(zhí)行。
dispatch_barrier_async柵欄方法
dispatch_barrier_async是oc的實現(xiàn),Swift的實現(xiàn)que.async(flags: .barrier)這樣。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//創(chuàng)建并發(fā)隊列l(wèi)et que = DispatchQueue.init(label: "com.jackyshan.thread", attributes: .concurrent)//并發(fā)異步執(zhí)行任務que.async {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//耗時操作print("任務0------", Thread.current)//打印線程}}//并發(fā)異步執(zhí)行任務que.async {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//耗時操作print("任務1------", Thread.current)//打印線程}}//柵欄方法:等待隊列里前面的任務執(zhí)行完之后執(zhí)行que.async(flags: .barrier) {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//耗時操作print("任務2------", Thread.current)//打印線程}//執(zhí)行完之后執(zhí)行隊列后面的任務}//并發(fā)異步執(zhí)行任務que.async {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//耗時操作print("任務3------", Thread.current)//打印線程}}print("代碼塊------結(jié)束") }打印結(jié)果: currentThread— <NSThread: 0x1c4078a00>{number = 1, name = main} 代碼塊------begin 代碼塊------結(jié)束 任務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執(zhí)行的barrier的操作,任務0和1交替執(zhí)行,任務2等待0和1執(zhí)行完才執(zhí)行,任務3也是等待任務2執(zhí)行完畢。 也可以看到由于barrier的操作,并沒有開啟新的線程去跑任務。
Quality Of Service(QoS)和優(yōu)先級
在使用 GCD 與 dispatch queue 時,我們經(jīng)常需要告訴系統(tǒng),應用程序中的哪些任務比較重要,需要更高的優(yōu)先級去執(zhí)行。當然,由于主隊列總是用來處理 UI 以及界面的響應,所以在主線程執(zhí)行的任務永遠都有最高的優(yōu)先級。不管在哪種情況下,只要告訴系統(tǒng)必要的信息,iOS 就會根據(jù)你的需求安排好隊列的優(yōu)先級以及它們所需要的資源(比如說所需的 CPU 執(zhí)行時間)。雖然所有的任務最終都會完成,但是,重要的區(qū)別在于哪些任務更快完成,哪些任務完成得更晚。
用于指定任務重要程度以及優(yōu)先級的信息,在 GCD 中被稱為 Quality of Service(QoS)。事實上,QoS 是有幾個特定值的枚舉類型,我們可以根據(jù)需要的優(yōu)先級,使用合適的 QoS 值來初始化隊列。如果沒有指定 QoS,則隊列會使用默認優(yōu)先級進行初始化。要詳細了解 QoS 可用的值,可以參考這個文檔,請確保你仔細看過這個文檔。下面的列表總結(jié)了 Qos 可用的值,它們也被稱為 QoS classes。第一個 class 代碼了最高的優(yōu)先級,最后一個代表了最低的優(yōu)先級:
- userInteractive
- userInitiated
- default
- utility
- background
- unspecified
創(chuàng)建兩個隊列,優(yōu)先級都是userInteractive,看看效果:
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//創(chuàng)建并發(fā)隊列1let que1 = DispatchQueue.init(label: "com.jackyshan.thread1", qos: .userInteractive, attributes: .concurrent)//創(chuàng)建并發(fā)隊列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("代碼塊------結(jié)束") }打印結(jié)果: currentThread— <NSThread: 0x1c0073680>{number = 1, name = main} 代碼塊------begin 代碼塊------結(jié)束 任務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)}
兩個隊列的優(yōu)先級一樣,任務也是交替執(zhí)行,這和我們預測的一樣。
下面把queue1的優(yōu)先級改為background,看看效果:
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//創(chuàng)建并發(fā)隊列1let que1 = DispatchQueue.init(label: "com.jackyshan.thread1", qos: .background, attributes: .concurrent)//創(chuàng)建并發(fā)隊列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("代碼塊------結(jié)束") }打印結(jié)果: currentThread— <NSThread: 0x1c006afc0>{number = 1, name = main} 代碼塊------begin 代碼塊------結(jié)束 任務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的優(yōu)先級調(diào)低為background,queue2的任務就優(yōu)先執(zhí)行了。
還有其他的優(yōu)先級,從高到低,就不一一相互比較了。
DispatchSemaphore信號量
GCD 中的信號量是指 Dispatch Semaphore,是持有計數(shù)的信號。類似于過高速路收費站的欄桿。可以通過時,打開欄桿,不可以通過時,關(guān)閉欄桿。在 Dispatch Semaphore 中,使用計數(shù)來完成這個功能,計數(shù)為0時等待,不可通過。計數(shù)為1或大于1時,計數(shù)減1且不等待,可通過。
- DispatchSemaphore(value: ):用于創(chuàng)建信號量,可以指定初始化信號量計數(shù)值,這里我們默認1.
- semaphore.wait():會判斷信號量,如果為1,則往下執(zhí)行。如果是0,則等待。
- semaphore.signal():代表運行結(jié)束,信號量加1,有等待的任務這個時候才會繼續(xù)執(zhí)行。
可以使用DispatchSemaphore實現(xiàn)線程同步,保證線程安全。
加入有一個票池,同時幾個線程去賣票,我們要保證每個線程獲取的票池是一致的。 使用DispatchSemaphore和剛才講的DispatchWorkItem來實現(xiàn),我們看看效果。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//創(chuàng)建票池var tickets = [Int]()for i in 0..<38 {tickets.append(i)}//創(chuàng)建一個初始計數(shù)值為1的信號let semaphore = DispatchSemaphore(value: 1)let workItem = DispatchWorkItem.init {semaphore.wait()if tickets.count > 0 {Thread.sleep(forTimeInterval: 0.2)//耗時操作print("剩余票數(shù)", tickets.count, Thread.current)tickets.removeLast()//去票池庫存}else {print("票池沒票了")}semaphore.signal()}//創(chuàng)建并發(fā)隊列1let que1 = DispatchQueue.init(label: "com.jackyshan.thread1", qos: .background, attributes: .concurrent)//創(chuàng)建并發(fā)隊列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("代碼塊------結(jié)束") }currentThread— <NSThread: 0x1c407a6c0>{number = 1, name = main} 代碼塊------begin 代碼塊------結(jié)束 剩余票數(shù) 38 <NSThread: 0x1c44706c0>{number = 8, name = (null)} 剩余票數(shù) 37 <NSThread: 0x1c0264c80>{number = 9, name = (null)} 剩余票數(shù) 36 <NSThread: 0x1c44706c0>{number = 8, name = (null)} … 剩余票數(shù) 19 <NSThread: 0x1c0264c80>{number = 9, name = (null)} 剩余票數(shù) 18 <NSThread: 0x1c44706c0>{number = 8, name = (null)} 剩余票數(shù) 17 <NSThread: 0x1c0264c80>{number = 9, name = (null)} 剩余票數(shù) 16 <NSThread: 0x1c44706c0>{number = 8, name = (null)} … 剩余票數(shù) 2 <NSThread: 0x1c44706c0>{number = 8, name = (null)} 剩余票數(shù) 1 <NSThread: 0x1c0264c80>{number = 9, name = (null)} 票池沒票了 票池沒票了
可以看到我們的資源沒有因為造成資源爭搶而出現(xiàn)數(shù)據(jù)紊亂。信號量很好的實現(xiàn)了多線程同步的功能。
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監(jiān)測底層的事件,這些事件包括Mach ports,Unix descriptors,Unix signals,VFS nodes。
Tips: DispatchSource這個class很好的體現(xiàn)了Swift是一門面向協(xié)議的語言。這個類是一個工廠類,用來實現(xiàn)各種source。比如DispatchSourceTimer(本身是個協(xié)議)表示一個定時器。
- DispatchSourceProtocol
基礎協(xié)議,所有的用到的DispatchSource都實現(xiàn)了這個協(xié)議。這個協(xié)議的提供了公共的方法和屬性: 由于不同的source是用到的屬性和方法不一樣,這里只列出幾個公共的方法
-
activate //激活
-
suspend //掛起
-
resume //繼續(xù)
-
cancel //取消(異步的取消,會保證當前eventHander執(zhí)行完)
-
setEventHandler //事件處理邏輯
-
setCancelHandler //取消時候的清理邏輯
-
DispatchSourceTimer
在Swift 3中,可以方便的用GCD創(chuàng)建一個Timer(新特性)。DispatchSourceTimer本身是一個協(xié)議。 比如,寫一個timer,1秒后執(zhí)行,然后10秒后自動取消,允許10毫秒的誤差
PlaygroundPage.current.needsIndefiniteExecution = truepublic let timer = DispatchSource.makeTimerSource()timer.setEventHandler {//這里要注意循環(huán)引用,[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也支持只調(diào)用一次。
func scheduleOneshot(deadline: DispatchTime, leeway: DispatchTimeInterval = default)- UserData
DispatchSource中UserData部分也是強有力的工具,這部分包括兩個協(xié)議,兩個協(xié)議都是用來合并數(shù)據(jù)的變化,只不過一個是按照+(加)的方式,一個是按照|(位與)的方式。
DispatchSourceUserDataAdd DispatchSourceUserDataOr
在使用這兩種Source的時候,GCD會幫助我們自動的將這些改變合并,然后在適當?shù)臅r候(target queue空閑)的時候,去回調(diào)EventHandler,從而避免了頻繁的回調(diào)導致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的一個抽象基類,將線程封裝成要執(zhí)行的操作,不需要管理線程的生命周期和同步,但比GCD可控性更強,例如可以加入操作依賴(addDependency)、設置操作隊列最大可并發(fā)執(zhí)行的操作個數(shù)(setMaxConcurrentOperationCount)、取消操作(cancel)等。NSOperation作為抽象基類不具備封裝我們的操作的功能,需要使用兩個它的實體子類:NSBlockOperation和繼承NSOperation自定義子類。NSOperation需要配合NSOperationQueue來實現(xiàn)多線程。
NSOperation使用步驟
自定義Operation
繼承Operation創(chuàng)建一個類,并重寫main方法。當調(diào)用start的時候,會在適當?shù)臅r候執(zhí)行main里面的任務。
class ViewController: UIViewController {@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")let op = JKOperation.init()op.start()print("代碼塊------結(jié)束")} }class JKOperation: Operation {override func main() {Thread.sleep(forTimeInterval: 0.2)//執(zhí)行耗時操作print("任務---", Thread.current)} }打印結(jié)果: currentThread— <NSThread: 0x1c007a280>{number = 1, name = main} 代碼塊------begin 任務— <NSThread: 0x1c007a280>{number = 1, name = main} 代碼塊------結(jié)束
可以看到自定義JKOperation,初始化之后,調(diào)用start方法,main方法里面的任務執(zhí)行了,是在主線程執(zhí)行的。 因為我們沒有使用OperationQueue,所以沒有創(chuàng)建新的線程。
使用BlockOperation
初始化BlockOperation之后,調(diào)用start方法。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")let bop = BlockOperation.init {Thread.sleep(forTimeInterval: 0.2)//執(zhí)行耗時操作print("任務---", Thread.current)}bop.start()print("代碼塊------結(jié)束") }打印結(jié)果: currentThread— <NSThread: 0x1c4070640>{number = 1, name = main} 代碼塊------begin 任務— <NSThread: 0x1c4070640>{number = 1, name = main} 代碼塊------結(jié)束
配合OperationQueue實現(xiàn)
初始化OperationQueue之后,調(diào)用addOperation,代碼塊就會自定執(zhí)行,調(diào)用機制執(zhí)行是有OperationQueue里面自動實現(xiàn)的。 addOperation的方法里面其實是生成了一個BlockOperation對象,然后執(zhí)行了這個對象的start方法。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")OperationQueue.init().addOperation {Thread.sleep(forTimeInterval: 0.2)//執(zhí)行耗時操作print("任務---", Thread.current)}print("代碼塊------結(jié)束") }打印結(jié)果: currentThread— <NSThread: 0x1c006a700>{number = 1, name = main} 代碼塊------begin 代碼塊------結(jié)束 任務— <NSThread: 0x1c0469900>{number = 5, name = (null)}
可以看到OperationQueue初始化,默認是生成了一個并發(fā)隊列,而且執(zhí)行的是一個異步操作,所以打印任務的線程不是在主線程。
隊列的創(chuàng)建方法/獲取方法
OperationQueue沒有實現(xiàn)串行隊列的方法,也沒有像GCD那樣實現(xiàn)了一個全局隊列。 只有并發(fā)隊列的實現(xiàn)和主隊列的獲取。
- 創(chuàng)建并發(fā)隊列
并發(fā)隊列的任務是并發(fā)(幾乎同時)執(zhí)行的,可以最大發(fā)揮CPU多核的優(yōu)勢。 看到有的說通過maxConcurrentOperationCount設置并發(fā)數(shù)量1就實現(xiàn)了串行。 實際上是不對的,通過設置優(yōu)先級可以控制隊列的任務交替執(zhí)行,在下面講到maxConcurrentOperationCount會實現(xiàn)代碼。
//創(chuàng)建并發(fā)隊列 let queue = OperationQueue.init()OperationQueue初始化,默認實現(xiàn)的是并發(fā)隊列。
- 獲取主隊列
我們的主隊列是串行隊列,任務是一個接一個執(zhí)行的。
//獲取主隊列 let queue = OperationQueue.main獲取主隊列的任務是異步執(zhí)行的。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//獲取主隊列l(wèi)et queue = OperationQueue.mainqueue.addOperation {Thread.sleep(forTimeInterval: 0.2)//執(zhí)行耗時操作print("任務1---", Thread.current)}queue.addOperation {Thread.sleep(forTimeInterval: 0.2)//執(zhí)行耗時操作print("任務2---", Thread.current)}print("代碼塊------結(jié)束") }打印結(jié)果: currentThread— <NSThread: 0x1c0064f80>{number = 1, name = main} 代碼塊------begin 代碼塊------結(jié)束 任務1— <NSThread: 0x1c0064f80>{number = 1, name = main} 任務2— <NSThread: 0x1c0064f80>{number = 1, name = main}
任務的創(chuàng)建方法
- 通過BlockOperation創(chuàng)建任務
- 通過OperationQueue創(chuàng)建任務
NSOperation相關(guān)方法
最大并發(fā)操作數(shù):maxConcurrentOperationCount
maxConcurrentOperationCount 默認情況下為-1,表示不進行限制,可進行并發(fā)執(zhí)行。 maxConcurrentOperationCount這個值不應超過系統(tǒng)限制(64),即使自己設置一個很大的值,系統(tǒng)也會自動調(diào)整為 min{自己設定的值,系統(tǒng)設定的默認最大值}。
- 設置maxConcurrentOperationCount為1,實現(xiàn)串行操作。
打印結(jié)果: currentThread— <NSThread: 0x1c0067880>{number = 1, name = main} 代碼塊------begin 代碼塊------結(jié)束 任務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)}
從打印結(jié)果可以看到隊列里的任務是按串行執(zhí)行的。 這是因為隊列里的任務優(yōu)先級一樣,在只有一個并發(fā)隊列數(shù)的時候,任務按順序執(zhí)行。
- 設置maxConcurrentOperationCount為1,實現(xiàn)并發(fā)操作。
currentThread— <NSThread: 0x1c4261780>{number = 1, name = main} 代碼塊------begin 代碼塊------結(jié)束 任務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)}
可以我們通過設置優(yōu)先級queuePriority,實現(xiàn)了隊列的任務交替執(zhí)行了。
- 設置maxConcurrentOperationCount為11,實現(xiàn)并發(fā)操作。
打印結(jié)果: currentThread— <NSThread: 0x1c407a200>{number = 1, name = main} 代碼塊------begin 代碼塊------結(jié)束 任務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的時候,實現(xiàn)了并發(fā)操作。
等待執(zhí)行完成:waitUntilFinished
waitUntilFinished阻塞當前線程,直到該操作結(jié)束。可用于線程執(zhí)行順序的同步。
比如實現(xiàn)兩個并發(fā)隊列按順序執(zhí)行。
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//創(chuàng)建并發(fā)隊列1let queue1 = OperationQueue.init()let bq1 = BlockOperation.init {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//執(zhí)行耗時操作print("任務1---", Thread.current)}}let bq2 = BlockOperation.init {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//執(zhí)行耗時操作print("任務2---", Thread.current)}}queue1.addOperations([bq1, bq2], waitUntilFinished: true)//創(chuàng)建并發(fā)隊列2let queue2 = OperationQueue.init()let bq3 = BlockOperation.init {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//執(zhí)行耗時操作print("任務3---", Thread.current)}}let bq4 = BlockOperation.init {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//執(zhí)行耗時操作print("任務4---", Thread.current)}}queue2.addOperations([bq3, bq4], waitUntilFinished: true)print("代碼塊------結(jié)束")}打印結(jié)果: 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)} 代碼塊------結(jié)束
通過設置隊列的waitUntilFinished為true,可以看到queu1的任務并發(fā)執(zhí)行完了之后,queue2的任務才開始并發(fā)執(zhí)行。 而且所有的執(zhí)行是在代碼塊------begin和代碼塊------結(jié)束之間的。queue1和queue2阻塞了主線程。
操作依賴:addDependency
@IBAction func onThread() {//打印當前線程print("currentThread---", Thread.current)print("代碼塊------begin")//創(chuàng)建并發(fā)隊列l(wèi)et queue = OperationQueue.init()let bq1 = BlockOperation.init {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//執(zhí)行耗時操作print("任務1---", Thread.current)}}let bq2 = BlockOperation.init {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//執(zhí)行耗時操作print("任務2---", Thread.current)}}let bq3 = BlockOperation.init {for _ in 0..<2 {Thread.sleep(forTimeInterval: 0.2)//執(zhí)行耗時操作print("任務3---", Thread.current)}}bq3.addDependency(bq1)queue.addOperations([bq1, bq2, bq3], waitUntilFinished: false)print("代碼塊------結(jié)束") }打印結(jié)果: currentThread— <NSThread: 0x1c0065740>{number = 1, name = main} 代碼塊------begin 代碼塊------結(jié)束 任務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)}
不添加操作依賴
打印結(jié)果: currentThread— <NSThread: 0x1c4072c40>{number = 1, name = main} 代碼塊------begin 代碼塊------結(jié)束 任務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,執(zhí)行就一直等待任務1完成。
優(yōu)先級:queuePriority
NSOperation 提供了queuePriority(優(yōu)先級)屬性,queuePriority屬性適用于同一操作隊列中的操作,不適用于不同操作隊列中的操作。默認情況下,所有新創(chuàng)建的操作對象優(yōu)先級都是NSOperationQueuePriorityNormal。但是我們可以通過setQueuePriority:方法來改變當前操作在同一隊列中的執(zhí)行優(yōu)先級。
public enum QueuePriority : Int {case veryLowcase lowcase normalcase highcase veryHigh }-
當一個操作的所有依賴都已經(jīng)完成時,操作對象通常會進入準備就緒狀態(tài),等待執(zhí)行。* queuePriority屬性決定了進入準備就緒狀態(tài)下的操作之間的開始執(zhí)行順序。并且,優(yōu)先級不能取代依賴關(guān)系。* 如果一個隊列中既包含高優(yōu)先級操作,又包含低優(yōu)先級操作,并且兩個操作都已經(jīng)準備就緒,那么隊列先執(zhí)行高優(yōu)先級操作。比如上例中,如果 op1 和 op4 是不同優(yōu)先級的操作,那么就會先執(zhí)行優(yōu)先級高的操作。* 如果,一個隊列中既包含了準備就緒狀態(tài)的操作,又包含了未準備就緒的操作,未準備就緒的操作優(yōu)先級比準備就緒的操作優(yōu)先級高。那么,雖然準備就緒的操作優(yōu)先級低,也會優(yōu)先執(zhí)行。優(yōu)先級不能取代依賴關(guān)系。如果要控制操作間的啟動順序,則必須使用依賴關(guān)系。##### NSOperation常用屬性和方法
-
取消操作方法
open func cancel()可取消操作,實質(zhì)是標記isCancelled狀態(tài)。
- 判斷操作狀態(tài)方法
open var isExecuting: Bool { get }判斷操作是否正在在運行。
open var isFinished: Bool { get }判斷操作是否已經(jīng)結(jié)束。
open var isConcurrent: Bool { get }判斷操作是否處于串行。
open var isAsynchronous: Bool { get }判斷操作是否處于并發(fā)。
open var isReady: Bool { get }判斷操作是否處于準備就緒狀態(tài),這個值和操作的依賴關(guān)系相關(guān)。
open var isCancelled: Bool { get }判斷操作是否已經(jīng)標記為取消。
- 操作同步
open func waitUntilFinished()阻塞當前線程,直到該操作結(jié)束。可用于線程執(zhí)行順序的同步。
open var completionBlock: (() -> Swift.Void)?會在當前操作執(zhí)行完畢時執(zhí)行 completionBlock。
open func addDependency(_ op: Operation)添加依賴,使當前操作依賴于操作 op 的完成。
open func removeDependency(_ op: Operation)移除依賴,取消當前操作對操作 op 的依賴。
open var dependencies: [Operation] { get }在當前操作開始執(zhí)行之前完成執(zhí)行的所有操作對象數(shù)組。
open var queuePriority: Operation.QueuePriority設置當前操作在隊列中的優(yōu)先級。
NSOperationQueue常用屬性和方法
- 取消/暫停/恢復操作
open func cancelAllOperations()可以取消隊列的所有操作。
open var isSuspended: Bool判斷隊列是否處于暫停狀態(tài)。true為暫停狀態(tài),false為恢復狀態(tài)。可設置操作的暫停和恢復,true代表暫停隊列,false代表恢復隊列。
- 操作同步
open func waitUntilAllOperationsAreFinished()阻塞當前線程,直到隊列中的操作全部執(zhí)行完畢。
- 添加/獲取操作
open func addOperation(_ block: @escaping () -> Swift.Void) 向隊列中添加一個 NSBlockOperation 類型操作對象。
open func addOperations(_ ops: [Operation], waitUntilFinished wait: Bool)向隊列中添加操作數(shù)組,wait 標志是否阻塞當前線程直到所有操作結(jié)束。
open var operations: [Operation] { get }當前在隊列中的操作數(shù)組(某個操作執(zhí)行結(jié)束后會自動從這個數(shù)組清除)。
open var operationCount: Int { get }當前隊列中的操作數(shù)。
- 獲取隊列
open class var current: OperationQueue? { get }獲取當前隊列,如果當前線程不是在 NSOperationQueue 上運行則返回 nil。
open class var main: OperationQueue { get } 獲取主隊列。
總結(jié)
以上是生活随笔為你收集整理的iOS多线程详解:实践篇的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 软件工程需求分析—结对
- 下一篇: 服务器的默认远程端口