iOS多執行緒的方法有3種:
- NSThread
- NSOperation
- GCD(Grand Central Dispatch)
其中,由蘋果所倡導的為多核的並行運算提出的解決方案:GCD能夠訪問執行緒池,並且可在應用的整個生命的週期裡面使用,一般來說,GCD會盡量維護一些適合機器體系結構的執行緒,在有工作需求的時候,自動利用更多的處理器核心,以此來充分使用更強大的機器系統效能。在以前,iOS裝置為單核處理器的,執行緒池的用處並不大,但是現在的移動裝置,包括iOS裝置,愈發地朝多核的方向邁進,因此GCD中的執行緒池,能夠在此類裝置中,能夠使得強大的硬體系統效能上得到更加完善的利用。
GCD,無疑是最便捷的,基於C
語言的所設計的。在使用GCD的過程中
,最方便的,莫過於不需要編寫基礎執行緒程式碼,其生命週期也不需要手動管理;建立需要的任務,然後新增到已建立好的queue佇列,GCD便
會負責建立執行緒和排程任務,由系統直接提供執行緒管理。
這樣一種多執行緒的方式,我們也會在實際專案中經常看到:app中,由於資料的執行與交換所消耗的時間長,導致需要反饋給使用者UI介面往往出現延遲的現象。這樣我們可以通過多執行緒的方法,讓需要呼叫的方法在後臺執行、在主執行緒上進行UI介面的切換,這樣不僅是使用者體驗更加友好美觀,也使得程式設計井然有序。
本文主要粗略介紹GCD的一般使用,以及GCD中dispatch_字首方法呼叫的作用和使用範圍。
UI介面如下圖,通過建立4個按鈕事件,分析4種不同的函式所執行的程式塊執行方式:
【本次開發環境: Xcode:7.2 iOS Simulator:iphone6 By:啊左】
一、GCD的使用
GCD對於開發者來說,最簡單的,就是通過呼叫dispatch把一連串的非同步任務新增到佇列中,進行非同步執行操作。
程式碼呼叫如下:
1 |
dispatch_async(dispatch_queue_t queue, dispatch_block_t block); |
- async表示非同步執行;
- queue為我們提前建立的佇列;
- block也就是“塊”,讓我們執行事件的模組;
async
(非同步)與
sync
(同步):
當然,我們也可以使用同步任務,使用dispatch_sync
函式新增到相應的佇列中,而這個函式會阻塞當前呼叫執行緒,直到相應任務完成執行。
但是,也正因為這樣的同步特性,在實際專案中,當有同步任務新增到正在執行同步任務的佇列時,序列的佇列會出現死鎖。而且由於同步任務會阻塞主執行緒的執行,可能會導致某個事件無法響應。
佇列(queue):
需要注意的是,呼叫dispatch_async不會讓塊執行,而是把塊新增到佇列末尾。佇列不是執行緒,它的作用是組織塊。(如果讀者學過資料結構的知識,就會知道佇列的基本特徵如飯堂排隊隊,先到的排前面,先打到飯,也就是“先進先出”原理)
在GCD中,可以給開發者呼叫的常見公共佇列有以下兩種:
dispatch_get_global_queue
:用於獲取應用全域性共享的併發佇列 (提供多個執行緒來執行任務,所以可以按序啟動多個任務併發執行。可用於後臺執行任務)dispatch_get_main_queue
: 用於獲取應用主執行緒關聯的序列排程佇列(只提供一個執行緒執行任務。執行的main主執行緒,一般用於UI的搭建)
(還有另外一種,dispatch_get_current_queue,
用於獲取當前正在執行任務的佇列,主要用於除錯,但是
在iOS 6.0
之後蘋果已經廢棄,原因是容易造成死鎖。詳情可以檢視官方註釋。)
這兩種公共佇列的呼叫便可以解決我們剛剛關於後臺執行任務、主執行緒用於更新UI介面的問題,
結構如下:
1 2 3 4 5 6 |
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 把邏輯計算等需要消耗長時間的任務,放在此處的全域性共享的併發佇列執行; dispatch_async(dispatch_get_main_queue(), ^{ // 回到主執行緒更新UI介面; }); }); |
例如在有一些專案中,會涉及到非同步下載圖片,這個時候就可以使用這樣一種結構來進行任務的分配:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 非同步下載圖片 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //先把下載資料的任務放在全域性共享併發佇列中執行 NSURL *url = [NSURL URLWithString:@"圖片的URL"]; NSData * data = [[NSData alloc]initWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; if(data != nil) { // 完成後,回到主執行緒顯示圖片 dispatch_async(dispatch_get_main_queue(), ^{ self.imageView.image = image; }); } }); |
二、序列佇列 and 並行佇列
1.序列(Serial)的執行:指同一時間每次只能執行一個任務。 執行緒池只提供一個執行緒用來執行任務,所以後一個任務必須等到前一個任務執行結束才能開始。
可以新增多個任務到佇列中,執行次序FIFO,但是當程式需要執行大量的任務時,雖然系統允許,但是鑑於程式的資源分配,應該交給全域性併發佇列來完成才能更好地發揮系統效能。
建立序列佇列的方式如下:
1 2 |
dispatch_queue_t serialQueue = dispatch_queue_create("zuoA", NULL); //第一個引數是佇列的名稱,通常使用公司的反域名;第二個引數是佇列相關屬性,一般用NULL. |
關於什麼是FIFO次序,我們用程式碼解釋一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- (IBAction)SerialQueue:(UIButton *)sender { dispatch_queue_t serialQueue = dispatch_queue_create("zuoA", NULL); dispatch_async(serialQueue, ^{ sleep(3); NSLog(@"A任務"); }); dispatch_async(serialQueue, ^{ sleep(2); NSLog(@"B任務"); }); dispatch_async(serialQueue, ^{ sleep(1); NSLog(@"C任務"); }); } |
console控制檯顯示如下:
1 2 3 |
2016-03-15 15:04:11.909 dispatch_queue的多工GCD使用[92316:2538875] A任務 2016-03-15 15:04:13.910 dispatch_queue的多工GCD使用[92316:2538875] B任務 2016-03-15 15:04:14.910 dispatch_queue的多工GCD使用[92316:2538875] C任務 |
可以看得到,即使需要等待幾秒,後面所新增的任務也必須等待前面的任務完成後才能執行,類似我們前面所講”飯堂”排隊的例子,佇列完全按照”先進先出”的順序,也即是所執行的順序取決於:開發者將工作任務新增進佇列的順序。
2.並行(concurrent)的執行:可同一時間可以同時執行多個任務。
- 負荷:併發執行任務與系統有關,能夠同時執行任務的數量是由系統根據應用和此時的系統狀態等動態變化決定的。
- 順序:由於並行佇列也是佇列(吐槽這是廢話T^T),因此每個任務的啟動時間也是按照FIFO次序,也就是加入queue的順序,但是結束的順序則依 賴各自的任務所需要消耗的時間。
- 與序列的不同:雖然啟動時間一致,但是這是“併發執行”,因此不需要等到上一個任務完成後才進行下一個任務。所以每個塊中的各部分的先後 執行的順序需要視情況而定。
上程式碼,找不同。。。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (IBAction)concurrentQueue:(UIButton *)sender { dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(concurrentQueue, ^{ dispatch_async(concurrentQueue, ^{ sleep(3); NSLog(@"A任務"); }); dispatch_async(concurrentQueue, ^{ sleep(2); NSLog(@"B任務"); }); dispatch_async(concurrentQueue, ^{ sleep(1); NSLog(@"C任務"); }); }); } |
console控制檯顯示如下:
1 2 3 |
2016-03-15 15:02:06.911 dispatch_queue的多工GCD使用[92294:2537296] C任務 2016-03-15 15:02:07.907 dispatch_queue的多工GCD使用[92294:2537147] B任務 2016-03-15 15:02:08.908 dispatch_queue的多工GCD使用[92294:2537177] A任務 |
通過控制檯左邊的時間記錄,可以看到,與序列佇列不同的是,並行佇列中這3個任務的並行啟用,與序列不同的是,不需要等到A任務呼叫完,就已經在呼叫B、C,顯著地提高了執行緒的執行速度,凸顯了並行佇列所執行的非同步操作的並行特性;
另外,從這段程式碼中,不同的是序列佇列需要建立一個新的佇列,而並行佇列中,只需要呼叫iOS系統中為我們提供的全域性共享dispatch_get_global_queue就可以了:
1 |
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
第一個引數為iOS系統為全域性共享佇列提供4種排程的方式,主要區別即是優先順序的不同而已:
- DISPATCH_QUEUE_PRIORITY_HIGH
- DISPATCH_QUEUE_PRIORITY_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW
- DISPATCH_QUEUE_PRIORITY_BACKGROUND
我們採用預設的DISPATCH_QUEUE_PRIORITY_DEFAULT方式,而右邊的第二個引數是蘋果預留的,暫時沒有其他的含義,所以,一般預設為:0。
併發的好處就是不需要像序列一樣按照順序執行,併發執行可以顯著地提高速度。
三、dispatch_group_async的使用
有時候,我們會遇到這樣的情況,UI介面部分的顯示,需要在完成幾個任務再進行主任務,例如3張圖片下載完畢,才通知UI介面已經完成任務。
我們可以通過分派組(dispatch group)進行併發程式塊分配的運用,將非同步分派(dispatch_async)的所有程式塊設定為鬆散,或者分配給多個執行緒來執行,監聽到這組任務全部完成後,使用dispatch_group_notify()通知並呼叫notify中的塊,例如UI介面的程式塊。
程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (IBAction)groupQueue:(UIButton *)sender { dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue, ^{ sleep(3); NSLog(@"A任務"); }); dispatch_group_async(group, queue, ^{ sleep(2); NSLog(@"B任務"); }); //group組中的任務完後,通知並呼叫notify中的塊 dispatch_group_notify(group, queue, ^{ NSLog(@"主任務"); }); } |
console控制檯顯示如下:
1 2 3 |
2016-03-16 11:18:41.306 dispatch_queue的多工GCD使用[94865:2718342] B任務 2016-03-16 11:18:42.302 dispatch_queue的多工GCD使用[94865:2718341] A任務 2016-03-16 11:18:42.303 dispatch_queue的多工GCD使用[94865:2718341] 主任務 |
結果驗證了前面說的,直到分派組任務都完後,notify新增的任務塊才會執行。
眼尖的讀者可能也發現,整個任務組完成的時間比2個任務分別執行的時間還要短!這得益於我們同時進行了兩種計算~
當然在真實的開發運用中,這種明顯執行時間縮短的效果,取決於所需要執行的工作量和可用的資源,以及多個CPU核心的可用性,因此在多核技術日益完善的大環境下,這樣一種多執行緒技術將得到更有效的利用。
四、dispatch_barrier_async的使用
dispatch_barrier(分派屏障)是當前面的任務執行完後,才執行barrier塊的任務,而且後面的任務也得等到barrier塊的執行完畢後才能開始執行。
很好地突顯了“障礙物”這樣的特性,那麼程式碼上應該怎麼寫呢?
按照併發的性質,我們在barrierQueue方法中敲入以下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (IBAction)barrierQueue:(UIButton *)sender { dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ sleep(2); NSLog(@"A任務"); }); dispatch_async(queue, ^{ sleep(1); NSLog(@"B任務"); }); dispatch_barrier_async(queue, ^{ NSLog(@"barrier任務"); }); dispatch_async(queue, ^{ sleep(1); NSLog(@"C任務"); }); } |
console控制檯顯示如下:
1 2 3 4 |
2016-03-16 13:18:47.525 dispatch_queue的多工GCD使用[95191:2752854] barrier任務 2016-03-16 13:18:48.529 dispatch_queue的多工GCD使用[95191:2752839] B任務 2016-03-16 13:18:48.529 dispatch_queue的多工GCD使用[95191:2752844] C任務 2016-03-16 13:18:49.528 dispatch_queue的多工GCD使用[95191:2752840] A任務 |
任務的執行順序依然是跟並行佇列的方法一樣,barrier沒有發揮它的“障礙物”的界限作用。這是因為barrier這一塊是依賴佇列queue的模型來執行的,當佇列為全域性共享時,barrier就無法發揮其作用。我們需要新建立一個佇列,
1 |
dispatch_queue_t queue = dispatch_queue_create("zuoA", DISPATCH_QUEUE_SERIAL); |
完整的程式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (IBAction)barrierQueue:(UIButton *)sender { dispatch_queue_t queue = dispatch_queue_create("zuoA", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ sleep(2); NSLog(@"A任務"); }); dispatch_async(queue, ^{ sleep(1); NSLog(@"B任務"); }); dispatch_barrier_async(queue, ^{ NSLog(@"barrier任務"); }); dispatch_async(queue, ^{ sleep(1); NSLog(@"C任務"); }); } |
console控制檯顯示如下:
1 2 3 4 |
2016-03-16 13:30:14.251 dispatch_queue的多工GCD使用[95263:2759658] A任務 2016-03-16 13:30:15.255 dispatch_queue的多工GCD使用[95263:2759658] B任務 2016-03-16 13:30:15.255 dispatch_queue的多工GCD使用[95263:2759658] barrier任務 2016-03-16 13:30:16.256 dispatch_queue的多工GCD使用[95263:2759658] C任務 |
這就是我們想要得到的效果:確實只有在前面A、B任務完成後,barrier任務才能執行,最後才能執行C任務。
那麼,dispatch_queue_create為什麼要用 DISPATCH_QUEUE_SERIAL,可以用其他麼?答案是肯定的。把引數換成DISPATCH_QUEUE_SERIAL
可以得到以下輸出:
1 2 3 4 |
2016-03-16 13:34:23.855 dispatch_queue的多工GCD使用[95294:2762604] B任務 2016-03-16 13:34:24.853 dispatch_queue的多工GCD使用[95294:2762603] A任務 2016-03-16 13:34:24.853 dispatch_queue的多工GCD使用[95294:2762603] barrier任務 2016-03-16 13:34:25.856 dispatch_queue的多工GCD使用[95294:2762603] C任務 |
也就是說,A、B、C任務完全是按照佇列的順序執行,只是由於barrier塊的“屏障”作用,把A、B任務放在前面,而使得後來加入的C任務只有等到barrier塊執行完畢才能執行;
五、dispatch_suspend(暫停)和 dispatch_resume(繼續)
- 暫停:當需要暫停某個佇列queue時, 呼叫dispatch_suspend(queue),此時阻止了queue執行塊物件,且
queue
的引用計數增加; - 繼續:繼續queue時,呼叫
dispatch_resume(queue),此時queue啟動執行塊的操作,
queue
的引用計數減少;
需要注意的是,suspend與resume是非同步的,只在block塊之間呼叫,而且必須是成對存在的。
還有一些其他的dispatch函式,例如
dispatch_once:可以使特定的塊在整個應用程式生命週期中只被執行一次~(在單例模式中使用到.)
dispatch_apply:執行某個程式碼片段n次(開發者可以自己設定)。
dispatch_after:當我們需要等待幾秒後進行某個操作,可以使用這個函式;
注意事項:
1.在上面的例子中,我們沒有使用過手動內管其記憶體,因為系統會自動管理。
如果你部署的最低目標低於 iOS 6.0 or Mac OS X 10.8
這個有興趣的童鞋可以瞭解一下。
(轉載請標明原文出處,謝謝支援 ~ ^-^ ~)
by:啊左~
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式