ios高級開發之多線程(三)GCD技術
GCD是基於C的API,它是libdispatch的的市場名稱。而libdispatch作為Apple公司的一個庫,為並發代碼在多核硬件(跑IOS或者OS X)上執行提供有力支持。
那麽我們為什麽要用GCD技術呢?
1.GCD能夠推遲昂貴的計算任務,並在後臺運行它們來改善你的應用的性能。
2.GCD提供一個易於使用的並發模型而不僅僅是鎖和線程。以幫助我們避開並發陷阱。
3.GCD具有在常見模式(比如單例)上用更高性能的原語優化你的代碼的潛在能力。
4.GCD旨在替換NSTread等線程技術。
5.GCD可充分利用設備的多核。
6.GCD可自動管理線程的生命周期。
說了這些GCD的優點,那麽在實際開發中,如何使用GCD來更好滿足我們的需求呢?
一、Synchronous&Asynchronous 同步&異步
1.Synchronous同步:同步任務的執行的方式:在當前線程中執行,必須等待當前語句執行完畢,才會執行下一條語句。
來看下同步的代碼:
//同步的打印順序 -(void)syncTask{ NSLog(@"begin"); //GCD的同步方法 dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //任務中要執行的代碼 [NSThread sleepForTimeInterval:2.0]; NSLog(@"%@",[NSThread currentThread]); }); NSLog(@"end"); }
來看打印出來的:
2019-03-29 12:00:54.993042+0800 wftest[5191:88411] begin
2019-03-29 12:00:56.994525+0800 wftest[5191:88411] <NSThread: 0x600000ff5380>{number = 1, name = main}
2019-03-29 12:00:56.994799+0800 wftest[5191:88411] end
可以看到,即使線程休眠了2秒,他依然會按照順序執行,等代碼塊內的代碼執行完畢後,才會執行end.
接著我們再來看異步,不在當前線程中執行,不用等當前語句執行完畢,就可以執行下一條語句
來看代碼:
//異步順序 -(void)asyncTask{ //異步不會在當前線程執行,首先需要開辟新的線程,而開辟新的線程也需要一定的時間 NSLog(@"begin"); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"%@",[NSThread currentThread]); }); NSLog(@"end"); }
我們來看下打印的情況:
2019-03-29 16:06:34.600530+0800 wftest[1336:19777] begin
2019-03-29 16:06:34.600763+0800 wftest[1336:19777] end
2019-03-29 16:06:34.600892+0800 wftest[1336:20090] <NSThread: 0x600000acc600>{number = 3, name = (null)}
可以看到,打印出來begin後,直接打印出了end.然後才執行了異步塊裏的代碼。
接下來,我們來看看串行隊列Serial queues,和並行隊列(並發隊列)Concurrent queues.
1.串行隊列的特點:
以先進先出的方式執行,順序調度隊列中的任務執行。
無論隊列中的任務函數是同步還是異步,都會等待前一個任務執行完成後,再調度後面的任務。
我們先來看看,串行隊列的同步代碼:
//串行隊列同步函數(在一個線程中執行,註意,GCD的是C語言API,不要和OC弄混) -(void)serialSync{ //這裏有兩個參數,第一個參數的標識符,一般為公司域名倒寫,第二個參數隊列類型,DISPATCH_QUEUE_SERIAL串行,DISPATCH_QUEUE_CONCURRENT為並發隊列 dispatch_queue_t serialQueue = dispatch_queue_create("com.feng", DISPATCH_QUEUE_SERIAL); //創建任務 void (^task1) (void) = ^(){ NSLog(@"task1---%@",[NSThread currentThread]); }; void (^task2) (void) = ^(){ NSLog(@"task2 -- %@",[NSThread currentThread]); }; void (^task3) (void) = ^(){ NSLog(@"task3 -- %@",[NSThread currentThread]); }; //添加任務到隊列,同步執行方法 dispatch_sync(serialQueue, task1); dispatch_sync(serialQueue, task2); dispatch_sync(serialQueue, task3); }
然後來看下NSLog打印的東西:
2019-03-29 17:08:10.362074+0800 wftest[2989:50297] task1---<NSThread: 0x600002009340>{number = 1, name = main}
2019-03-29 17:08:10.364550+0800 wftest[2989:50297] task2 -- <NSThread: 0x600002009340>{number = 1, name = main}
2019-03-29 17:08:10.365860+0800 wftest[2989:50297] task3 -- <NSThread: 0x600002009340>{number = 1, name = main}
可以看到task1,taks2,task3是完全按照順序執行的。
再來看串行隊列的異步方法:
先來看代碼
//串行隊列異步函數 -(void)serialAsync{ //創建一個串行隊列 dispatch_queue_t serialQuene = dispatch_queue_create("com.feng", DISPATCH_QUEUE_SERIAL); //2.創建任務 void (^task1)(void) = ^(){ NSLog(@"task1 --- %@",[NSThread currentThread]); }; void (^task2)(void) = ^(){ NSLog(@"task 2-- %@",[NSThread currentThread]); }; void (^task3)(void) = ^(){ NSLog(@"task 3 -- %@",[NSThread currentThread]); }; //3.添加任務隊列 dispatch_async(serialQuene, task1); dispatch_async(serialQuene, task2); dispatch_async(serialQuene, task3); }
來看下打印結果:
2019-03-30 14:27:21.730761+0800 wftest[4929:84017] 主線程 -- <NSThread: 0x600000650540>{number = 1, name = main} 2019-03-30 14:27:21.731252+0800 wftest[4929:84090] task1 --- <NSThread: 0x600000638340>{number = 3, name = (null)} 2019-03-30 14:27:21.731536+0800 wftest[4929:84090] task 2-- <NSThread: 0x600000638340>{number = 3, name = (null)} 2019-03-30 14:27:21.731691+0800 wftest[4929:84090] task 3 -- <NSThread: 0x600000638340>{number = 3, name = (null)}
可以看到,串行隊列異步執行,仍然按順序執行的。也就是說,只要是串行隊列,無論是異步,還是同步函數,都是按順序執行的。
2.看完串行隊列,我們來看並發(並行)隊列。
並發隊列的特點:
1.以先進先出的方法,並發調度隊列中的任務的執行。
2.如果是並發隊列的同步執行,就會等先被調度的任務執行完畢後,再執行下一個任務。
3.如果是並發隊列的異步執行,同時底層線程池有可用的線程資源,會在新的任務調度後,調度下一個任務。
也就是說,先加進來的任務會先被執行,但不用等他執行完畢,就可以接著調度下一個任務。
那麽,我們先來看並發隊列的同步任務的代碼:
//並發隊列同步函數 -(void)concurrentSync{ //1.創建並發隊列 dispatch_queue_t concurrentQueue = dispatch_queue_create("com.feng", DISPATCH_QUEUE_CONCURRENT); //2.創建任務 void (^task1) (void) = ^(){ NSLog(@"task1 -- %@",[NSThread currentThread]); }; void (^task2) (void) = ^(){ NSLog(@"task2 -- %@",[NSThread currentThread]); }; void (^task3) (void) = ^(){ NSLog(@"task3 -- %@",[NSThread currentThread]); }; //3.添加同步任務到並發隊列 dispatch_sync(concurrentQueue, task1); dispatch_sync(concurrentQueue, task2); dispatch_sync(concurrentQueue, task3); }
再來看打印情況:
2019-03-30 14:01:51.358796+0800 wftest[4073:69627] 主線程 -- <NSThread: 0x600001bdcd00>{number = 1, name = main} 2019-03-30 14:01:51.359220+0800 wftest[4073:69627] task1 -- <NSThread: 0x600001bdcd00>{number = 1, name = main} 2019-03-30 14:01:51.359668+0800 wftest[4073:69627] task2 -- <NSThread: 0x600001bdcd00>{number = 1, name = main} 2019-03-30 14:01:51.360195+0800 wftest[4073:69627] task3 -- <NSThread: 0x600001bdcd00>{number = 1, name = main}
可以看到,雖然是並發隊列,但因為是同步任務,所以也是按順序執行的。因為是同步任務,所以就在當前線程,主線程中執行。異步任務則會在子線程中執行。
(可以簡單總結:串行 ,要等待上個任務執行完畢,才執行下個任務,所以會在同一個線程中執行。 並行:不用等上個任務執行完畢,就可以執行下個任務。同步:在當前線程中執行,不會開辟子線程。異步:在子線程中執行(這是指串行和並行隊列。後面說的主隊列異步,也是在主線程中執行))。
再來看並發隊列的異步執行任務:
//並發隊列的異步執行 -(void)concurrentAsyn{ //1.創建隊列 dispatch_queue_t concurrentQueue = dispatch_queue_create("com.feng", DISPATCH_QUEUE_CONCURRENT); //2.創建任務 void(^task1) (void) = ^(){ NSLog(@"task1 -- %@",[NSThread currentThread]); }; void(^task2) (void) = ^(){ NSLog(@"task2 -- %@",[NSThread currentThread]); }; void(^task3) (void) = ^(){ NSLog(@"task3 -- %@",[NSThread currentThread]); }; //3.把任務添加到隊列中去 dispatch_async(concurrentQueue, task1); dispatch_async(concurrentQueue, task2); dispatch_async(concurrentQueue, task3); }
來看打印情況:
2019-03-30 14:02:45.003384+0800 wftest[4112:70392] 主線程 -- <NSThread: 0x600000ba8c40>{number = 1, name = main} 2019-03-30 14:02:45.005834+0800 wftest[4112:70450] task2 -- <NSThread: 0x600000bf7840>{number = 4, name = (null)} 2019-03-30 14:02:45.005834+0800 wftest[4112:70449] task1 -- <NSThread: 0x600000bcc380>{number = 3, name = (null)} 2019-03-30 14:02:45.005838+0800 wftest[4112:70454] task3 -- <NSThread: 0x600000bcc480>{number = 5, name = (null)}
可以看到,異步任務另外開辟了子線程。可以看到打印順序發生了變化。
接著,我們來看全局隊列。
全局隊列的工作表現和並發隊列一致。
但是全局隊列是否就是並發隊列呢?不是的。我們來看下他們的區別:
1.全局隊列沒有名稱,無論是MRC&ARC都不用考慮釋放,所以在日常開發中,建議使用全局隊列。
2.並發隊列:有名字,和NSThread的name屬性作用類似,如果你在MRC的開發中,則需要使用dispatch_releas(q)來釋放對應的對象。
那麽並發隊列在什麽時候使用呢?在你開發第三方的框架的時候,則需要使用並發隊列了。這樣可以避開和使用你的開發框架的程序員弄混隊列。
咱們先來看下全局隊列的同步任務。(日常開發中幾乎用不到。)
//全局隊列的同步任務 -(void)globalSync{ NSLog(@"begin"); //1.創建全局隊列 dispatch_queue_t gloabalQueue = dispatch_get_global_queue(0, 0); //2.創建任務 void(^task1) (void) = ^(){ NSLog(@"task1 -- %@",[NSThread currentThread]); }; void(^task2) (void) = ^(){ NSLog(@"task2 -- %@",[NSThread currentThread]); }; void(^task3) (void) = ^(){ NSLog(@"task3 -- %@",[NSThread currentThread]); }; //3.加入任務到隊列中執行 dispatch_sync(gloabalQueue, task1); dispatch_sync(gloabalQueue, task2); dispatch_sync(gloabalQueue, task3); NSLog(@"end"); }
來看打印結果:
2019-03-30 14:35:08.160967+0800 wftest[5189:88230] 主線程 -- <NSThread: 0x600000684300>{number = 1, name = main} 2019-03-30 14:35:08.161223+0800 wftest[5189:88230] begin 2019-03-30 14:35:08.161470+0800 wftest[5189:88230] task1 -- <NSThread: 0x600000684300>{number = 1, name = main} 2019-03-30 14:35:08.161639+0800 wftest[5189:88230] task2 -- <NSThread: 0x600000684300>{number = 1, name = main} 2019-03-30 14:35:08.161773+0800 wftest[5189:88230] task3 -- <NSThread: 0x600000684300>{number = 1, name = main} 2019-03-30 14:35:08.161891+0800 wftest[5189:88230] end
可以看到,按熟悉執行,同步的,都是當前線程中執行的。
再來看全局隊列的異步任務,他是在子線程池上執行的,每個任務都有一個自己的線程,前提是線程池裏有線程資源,底層有一個線程重用機制的。看下代碼:
2019-03-30 14:39:06.429812+0800 wftest[5330:90708] 主線程 -- <NSThread: 0x600003606580>{number = 1, name = main} 2019-03-30 14:39:06.430060+0800 wftest[5330:90708] begin 2019-03-30 14:39:06.430228+0800 wftest[5330:90708] end 2019-03-30 14:39:06.430381+0800 wftest[5330:90762] task1 -- <NSThread: 0x600003660a40>{number = 3, name = (null)} 2019-03-30 14:39:06.430416+0800 wftest[5330:90763] task3 -- <NSThread: 0x600003660a00>{number = 4, name = (null)} 2019-03-30 14:39:06.430422+0800 wftest[5330:90760] task2 -- <NSThread: 0x600003660ec0>{number = 5, name = (null)}
可以看到,每個任務都有自己的獨立的線程。
有點累,一會再來看看主隊列。
主隊列的特點:
1.專門用來在主線程上調度任務的隊列。
2.不會開啟子線程
3.以先進先出的方式,在主線程空閑的時候才會調度主隊列中的任務在主線程中執行。
4.如果當前主線程中有任務在執行,那麽無論主隊列中添加了什麽任務,都不會被調度。
主隊列是負責在主線程中調度任務的。
會隨著程序啟動一起創建。
對於我們程序員來說,主隊列只需要獲取,不需要創建。
那麽我們來看下主隊列的異步任務的代碼:
//主隊列的異步任務 -(void)mainAsync{ NSLog(@"begin"); //1.創建主隊列 dispatch_queue_main_t mainAsync = dispatch_get_main_queue(); //2.創建任務 void(^task1) (void) = ^(){ NSLog(@"task1 -- %@",[NSThread currentThread]); }; void(^task2) (void) = ^(){ NSLog(@"task2 -- %@",[NSThread currentThread]); }; void(^task3) (void) = ^(){ NSLog(@"task3 -- %@",[NSThread currentThread]); }; //添加任務到隊列 dispatch_async(mainAsync, task1); dispatch_async(mainAsync, task2); dispatch_async(mainAsync, task3); NSLog(@"end"); }
來看看打印情況:
2019-04-01 16:23:52.002922+0800 wftest[2320:37104] 主線程 -- <NSThread: 0x600001676a80>{number = 1, name = main} 2019-04-01 16:23:52.003178+0800 wftest[2320:37104] begin 2019-04-01 16:23:52.003342+0800 wftest[2320:37104] end 2019-04-01 16:23:52.092118+0800 wftest[2320:37104] task1 -- <NSThread: 0x600001676a80>{number = 1, name = main} 2019-04-01 16:23:52.092324+0800 wftest[2320:37104] task2 -- <NSThread: 0x600001676a80>{number = 1, name = main} 2019-04-01 16:23:52.092497+0800 wftest[2320:37104] task3 -- <NSThread: 0x600001676a80>{number = 1, name = main}
大家可以註意到這裏的幾個情況:
1.雖然是異步的,但是三個任務仍然按順序調度執行。
2.先執行了begin,緊接著執行的了end.然後才執行了三個任務,也就是說,主線程有空閑的時候才執行這三個任務。
我們說下deadlock死鎖:是兩個或者更多的線程之間出現的情況:比如第一個線程在等待第二個線程的完成才能繼續執行,而第二個線程在等待第一個線程的完成才能繼續執行。
看起來似乎異步更有用,效率更高,那麽同步有什麽用呢,我們說下同步的作用。
1.首先,同步肯定是保證了任務執行的順序。
2.可以讓後面的異步任務要依賴於某一個同步的任務。比如,必須讓用戶登錄之後,才允許他下載電影。
我們看下代碼:
//同步+異步 -(void)loadMovies{ dispatch_async(dispatch_get_global_queue(0, 0), ^{//開辟一條子線程 NSLog(@"開辟了子線程----%@",[NSThread currentThread]); dispatch_sync(dispatch_get_global_queue(0, 0), ^{ //登錄,在當前的線程執行 NSLog(@"登錄了---%@", [NSThread currentThread]); sleep(3); }); //2.同時下載3部電影 dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"正在下載第一部電影---%@",[NSThread currentThread]); }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"正在下載第二部電影---%@",[NSThread currentThread]); }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"正在下載第三部電影---%@",[NSThread currentThread]); }); dispatch_sync(dispatch_get_main_queue(), ^{ [NSThread sleepForTimeInterval:1.0]; NSLog(@"計算機將在三秒後關閉 --%@",[NSThread currentThread]); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"關機了---%@", [NSThread currentThread]); }); }); }); }
然後我們來看打印log:
2019-04-01 17:52:46.491858+0800 wftest[5359:81060] 開辟了子線程----<NSThread: 0x600002ff8580>{number = 3, name = (null)} 2019-04-01 17:52:46.492263+0800 wftest[5359:81060] 登錄了---<NSThread: 0x600002ff8580>{number = 3, name = (null)} 2019-04-01 17:52:49.497610+0800 wftest[5359:81063] 正在下載第一部電影---<NSThread: 0x600002ffaa00>{number = 4, name = (null)} 2019-04-01 17:52:49.497634+0800 wftest[5359:81062] 正在下載第三部電影---<NSThread: 0x600002ffefc0>{number = 6, name = (null)} 2019-04-01 17:52:49.497654+0800 wftest[5359:81061] 正在下載第二部電影---<NSThread: 0x600002ffef40>{number = 5, name = (null)} 2019-04-01 17:52:50.498825+0800 wftest[5359:80998] 計算機將在三秒後關閉 --<NSThread: 0x600002f9d600>{number = 1, name = main} 2019-04-01 17:52:53.752223+0800 wftest[5359:80998] 關機了---<NSThread: 0x600002f9d600>{number = 1, name = main}
我們註意到這幾個方面:雖然下載電影的時候,又開啟了三個新的線程,但是他們仍然要等待登錄後,才能執行,以及最後,計算機回到主隊列去關閉計算機的時候,也是等電影下載完畢。這是因為主隊列這裏的也是同步任務。前面也是同步任務。
接下來我們來看下dispatch_time的延遲操作
什麽時候使用dispatch_after呢?
1.最好堅持在主隊列上使用dispatch_after。而不是在自定義串行隊列上,並發隊列也盡量不要使用。
2.主隊列(串行)是使用dispatch_after的最好選擇。xcode也提供了自動完成模板。
我們來看下代碼:
//延遲執行 -(void)delay{ dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)); void(^task)(void)=^(){ NSLog(@"%@",[NSThread currentThread]); }; //主隊列 dispatch_after(when, dispatch_get_main_queue(), task); NSLog(@"come here"); }
來看打印:
2019-04-02 10:55:47.020688+0800 wftest[2069:28686] come here 2019-04-02 10:55:49.216037+0800 wftest[2069:28686] <NSThread: 0x6000018e1c40>{number = 1, name = main}
IOS提供的一些方便使用的延遲:
//延遲執行 -(void)after{ [self.view performSelector:@selector(setBackgroundColor:) withObject:[UIColor orangeColor] afterDelay:1.0]; }
再來看看線程安全:
線程安全是多線程不可避免的問題。
dispatch_once以線程安全的方式執行,僅且執行代碼一次。她會給代碼設立一個臨界區。試圖訪問臨界區(即要傳遞給dispatch_onece的代碼)的不同線程,在臨界區已經有一個線程在執行的情況下會被阻塞,直到臨界區完成為止。
我們來看下使用dispatch_once來實現單例線程安全:
//使用dispatch_once實現線程安全的單例 +(instancetype)sharedSingleton{ static id instance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; }
如果一個單例中的單例屬性是一個可變對象。那麽就要考慮線程安全問題了。比如NSMutableArray。可能會出現一個線程正在讀取,另外一個線程正在修改。這樣就會出現線程不安全的情況。在GCD中可以通過dispatch_barrier_async來進行創建讀寫鎖,這樣一個解決方案。
接下來,我們來看下調度組(dispatch_group):
調度組的實現原理:類似引用計數,進行+1,-1;
應用場景:
比如當你開啟了下載任務,當下載三個任務,只有等這三個任務全部下載完畢後,才能下一步做事情。這個時候就可以用到調度組,這個調度組,就能監聽它裏面的任務是否執行完畢:
//調度組 -(void)groupDispatch{ //1.創建調度組 dispatch_group_t group = dispatch_group_create(); //2.獲取全局隊列 dispatch_queue_t queue = dispatch_get_global_queue(0, 0); //3.創建三個下載任務 void(^task1)(void) = ^(){ NSLog(@"%@----下載片頭",[NSThread currentThread]); }; dispatch_group_enter(group);//引用計算+1 void (^task2) (void) = ^(){ NSLog(@"%@---下載內容",[NSThread currentThread]); [NSThread sleepForTimeInterval:3.0]; NSLog(@"----下載內容完畢"); dispatch_group_leave(group);//引用計數-1 }; dispatch_group_enter(group);//引用計數+1 void(^task3)(void)=^(){ NSLog(@"%@----下載片尾",[NSThread currentThread]); dispatch_group_leave(group);//引用計數-1 }; //4.需要將我們的隊列和任務放到組內去監控 dispatch_group_async(group, queue, task1); dispatch_group_async(group, queue, task2); dispatch_group_async(group, queue, task3); //5.監聽函數 // 參數2,表示參數3這裏的代碼在哪個隊列中執行 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ //表示組內所有的任務都完成之後,執行這裏的代碼 NSLog(@"把下載好的視頻按順序拼接好,然後顯示在UI上播放%@",[NSThread currentThread]); }); }
看打印:
2019-04-02 16:18:45.168288+0800 wftest[12368:305146] <NSThread: 0x6000023d5fc0>{number = 4, name = (null)}---下載內容 2019-04-02 16:18:45.168288+0800 wftest[12368:305147] <NSThread: 0x6000023d5f00>{number = 3, name = (null)}----下載片頭 2019-04-02 16:18:45.168288+0800 wftest[12368:305149] <NSThread: 0x6000023e9c80>{number = 5, name = (null)}----下載片尾 2019-04-02 16:18:48.174292+0800 wftest[12368:305146] ----下載內容完畢 2019-04-02 16:18:48.174604+0800 wftest[12368:305085] 把下載好的視頻按順序拼接好,然後顯示在UI上播放<NSThread: 0x6000023b25c0>{number = 1, name = main}
dispatch_group_enter手動通知group任務已經開始。註意,enter和leave必須成對。否則會造成詭異崩潰問題。
最後,再來看定時源事件和子線程的運行循環:
-(void)myMain{ //1.定義一個定時器 NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeEvnet) userInfo:nil repeats:YES]; //2.將定時器加入到運行循環中,只有當加入到運行循環中,他才知道這個時候,有一個定時任務 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; } -(void)timeEvnet{ NSLog(@"%d----%@",self.count,[NSThread currentThread]); if(self.count++ == 10){ NSLog(@"掛了"); //停止當前的運行循環 CFRunLoopStop(CFRunLoopGetCurrent()); } }
ios高級開發之多線程(三)GCD技術