GCD
GCD簡介
- Grand Central Dispatch中樞調度器
- 純C語言的,提供了非常強大的函數
- 優勢
- GCD是蘋果公司為多核的并行運算提出的解決方案
- GCD會自動利用更多的CPU內核(比如雙核、四核),充分利用設備的多核
- GCD會自動管理線程的生命周期(創建線程、調度任務、銷毀線程)
- github上面有源碼
任務和隊列
- GCD中的核心概念
- 任務:執行什么操作,需要用函數封裝起來(同步函數|異步函數)
- 隊列:用來存放任務的
- 使用步驟
- 定制任務
- 確定想要做的事情
- 將任務添加到隊列中
- GCD會自動將隊列中的任務取出,放到對應的線程中執行
- 任務的取出遵循隊列的FIFO原則:先進先出,后進后出
- 定制任務
- 執行任務
- 同步
- dispatch_sync(dispatch_queue_t queue,dispatch_block_t block)
- 只能在當前線程中執行任務,不具備開啟新線程的能力
- 異步
- dispatch_asyn
- 可以在新的線程中執行任務,具備開啟新線程的能力
- 同步
- 隊列的類型
- 并發隊列
- 可以讓多個任務并發執行
- 并發功能只有在異步函數下才有效
- 串行隊列
- 讓任務一個接著一個的執行
- 并發隊列
- 容易混淆的術語
- 同步異步:能不能開啟新的線程
- 同步:只是在當前線程中執行任務,不具備開啟新線程的能力
- 異步:可以在新的線程中執行任務,具備開啟新線程的能力
- 并發|串行:任務的執行方式
- 并發:允許多個任務并發執行
- 串行:一個任務執行完畢之后,再執行下一個任務
- 同步異步:能不能開啟新的線程
GCD基本使用
四種組合方式
-
異步函數+并發隊列
- 獲得隊列
- dispatch_queue_t 結構體
- dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_CONCURRENT)創建
- 第一個參數:C語言的字符串,給隊列起一個名字(推薦寫法:公司域名倒過來寫,后面跟上作用)名字的作用:是用來調試的
- 第二個參數:隊列的類型,傳一個宏
- DISPATCH_QUEUE_CONCURRENT
- DISPATCH_QUEUE_SERIAL
- 封裝任務并且把任務添加到隊列中
- dispatch_async(queue,任務)
- 第一個參數:隊列
- 第二個參數:block可以封裝一個任務
- 封裝任務
- 把任務添加到隊列中
- dispatch_async(queue,任務)
- 會開多條線程,所有的任務是并發執行的
- 注意:使用GCD的時候,具體開幾條線程,并不是由任務的數量來決定的,是由系統自動決定
- 會看CPU的處理情況,線程是可以復用的
- 獲得隊列
-
異步函數+串行隊列
- 獲得隊列
- dispatch_queue_t 結構體
- dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_SERIAL)創建
- 第一個參數:C語言的字符串,給隊列起一個名字(推薦寫法:公司域名倒過來寫,后面跟上作用)
- 第二個參數:隊列的類型,傳一個宏
- DISPATCH_QUEUE_CONCURRENT
- DISPATCH_QUEUE_SERIAL
- 封裝任務并且把任務添加到隊列中
- dispatch_async(queue,任務)
- 第一個參數:隊列
- 第二個參數:block可以封裝一個任務
- 封裝任務
- 把任務添加到隊列中
- dispatch_async(queue,任務)
- 會開啟一條子線程,所有的任務都是串行執行的,需要等前一個任務執行完,后面的任務才會執行
- 獲得隊列
-
同步函數+并行隊列
- 獲得隊列
- dispatch_queue_t 結構體
- dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_CONCURRENT)創建
- 第一個參數:C語言的字符串,給隊列起一個名字(推薦寫法:公司域名倒過來寫,后面跟上作用)
- 第二個參數:隊列的類型,傳一個宏
- DISPATCH_QUEUE_CONCURRENT
- DISPATCH_QUEUE_SERIAL
- 封裝任務并且把任務添加到隊列中
- dispatch_sync(queue,任務)
- 第一個參數:隊列
- 第二個參數:block可以封裝一個任務
- 封裝任務
- 把任務添加到隊列中
- dispatch_sync(queue,任務)
- 不會開啟新的線程,所有的任務都在當前線程中串行執行
- 獲得隊列
-
同步函數+串行隊列
- 獲得隊列
- dispatch_queue_t 結構體
- dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_SERIALRENT)創建
- 第一個參數:C語言的字符串,給隊列起一個名字(推薦寫法:公司域名倒過來寫,后面跟上作用)
- 第二個參數:隊列的類型,傳一個宏
- DISPATCH_QUEUE_CONCURRENT
- DISPATCH_QUEUE_SERIAL
- 封裝任務并且把任務添加到隊列中
- dispatch_sync(queue,任務)
- 第一個參數:隊列
- 第二個參數:block可以封裝一個任務
- 封裝任務
- 把任務添加到隊列中
- dispatch_sync(queue,任務)
- 不會開啟新的線程,所有的任務都在當前線程中串行執行
- 獲得隊列
同步函數不管是串行還是并發都是串行執行任務的。
全局并發隊列
-
并發隊列
- 隊列中的任務可以同時執行
- 并發隊列分為兩種
- 自己創建的并發隊列dispatch_queue_create
- 全局并發隊列(默認存在)
GCD默認已經提供了全局的并發隊列,供整個應用使用,可以無需手動創建
-
dispatch_get_global_queue函數獲得全局的并發隊列
- 參數一:隊列的優先級dispatch_queue_priority
- DISPATCH_QUEUE_PRIORITY_DEFAULT 默認優先級 == 0 ,不要傳其他的,會引發線程安全問題(反轉)
- 參數二:暫時無用,用0即可,給未來使用的
- 參數一:隊列的優先級dispatch_queue_priority
注意:線程的數量并不是由任務的數量決定的(是由系統決定的)
主隊列
- 串行隊列
- 必須一個接著一個的執行,要等當前任務執行完畢之后,才能執行后面的任務
- 串行隊列有兩種
- 自己創建的串行隊列dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_SERIAL)
- DISPATCH_QUEUE_SERIAL == NULL
- 主隊列(和主線程相關)
- 主隊列是GCD自帶的一種特殊的串行隊列
- 放在主隊列中的任務,都會放到主線程中執行 | 調度的時候比較特殊!
- dispatch_get_main_queue()獲得主隊列
- 自己創建的串行隊列dispatch_queue_create("com.520it,www.download",DISPATCH_QUEUE_SERIAL)
- 特點:
- 凡是放在主隊列中的任務都必須要在主線程中執行
- 本身是一個串行隊列,串行隊列的特點它都有
- 如果主隊列中有任務需要執行,那么主隊列會安排主線程來執行當前隊列中的任務,但是在調度之前,會先檢查主線程的狀態,如果主線程空閑,如果主線程在忙,就暫停調度隊列中的任務,等到主線程空閑的時候再調度。
- 主隊列+異步函數
- 獲得主隊列
- dispatch_get_main_queue()
- 添加任務到隊列中
- dispatch_async()
- 不會開線程,所有的任務都在
主線程
中串行執行
- 獲得主隊列
- 主隊列+同步函數
- 獲得主隊列
- dispatch_get_main_queue()
- 添加任務到隊列中
- dispatch_sync()
- 任務并沒有執行,有一個死鎖
- 主隊列里的任務必須要在主線程中執行
- 主線程在執行同步函數
- 主隊列工作模式:如果發現隊列中有任務,那么就安排主線程來執行,但是在安排之前, 會先檢查主線程的狀態(看主線程是否空閑),如果主線程在忙,那么就暫停調度,直到主線程空閑
- 如果在子線程中調度就不會死鎖
- performSelectorInBackground:@selector(主隊列+同步函數) withObject:
- 不會發生死鎖了,為什么?
- 主線程空閑,沒有事情做,主線程就執行了任務
- 獲得主隊列
開發中,常用的是:異步函數+并發隊列|串行隊列,面試中會把代碼給你,問你打印的結果是什么
-
問題:同步函數+主隊列會死鎖?異步函數+主隊列不會死鎖?
- 同步異步:
- 是否具有開線程的能力
- 同步函數同步執行任務,異步函數異步執行任務
- 同步執行:
- 必須要等當前任務執行完,才能執行后面的任務,如果我沒有執行,那么我后面的也別想執行
- 異步執行:
- 我開始執行后,可以不用等我執行完,就執行后面的任務
- 異步函數+主隊列不會發生死鎖
- 獲得一個主隊列
- 同步函數
- 封裝一個任務
- 把任務添加到隊列中去,檢查主線程狀態(較忙)死鎖
- 異步函數
- 隊列中有任務,要求主線程去執行
- 異步函數異步執行,跳過任務,把任務存在隊列中
- 代碼執行完畢后,主線程空閑了,就回來再執行隊列里的任務,就不會發生死鎖
- 同步異步:
- 異步函數 +并發隊列 ps 同步函數+并發隊列
- 1.start - end - 任務 - 異步
- 2.start - 任務 - end - 同步
-
問題:同步函數+串行隊列不會發生死鎖?
- 串行隊列,在當前線程中執行,串行隊列有任務就會執行,不會檢查主線程的狀態, 不管線程是否在忙,都會強行讓線程來執行任務
-
總結
- 異步函數+不是主隊列就一定會開線程
- 同步+并發:不會線程,串行執行任務
- 同步+串行:不會開線程,串行執行任務
- 同步+主隊列:不會開線程,死鎖
- 異步+并發:開多條線程,并發執行任務
- 異步+串行:開線程,串行執行任務
- 異步+主隊列:沒有開啟新線程,串行執行任務
線程間通信
- 開子線程下載圖片
- 異步+非主隊列
- 獲得全局并發隊列
- dispatch_get_global_queue(0,0)
- 使用異步函數+并發隊列
- dispatch_async(queue,block)
- 確定URL
- 把圖片的二進制數據下載到本地
- dataWithContentsOfURL:
- 轉化格式
- imageWithData:
- 顯示圖片
- 回到主線程刷新UI
- GCD可以嵌套
- dispatch_async(dispatch_get_main_queue(),^{
self.imageV.image = image;
})
- dispatch_async(queue,block)
- 配置info.plist文件
- 問:回到主線程,是異步函數+主隊列,在這里可以用同步函數+主隊列嗎?
- 可以,當前執行的是子線程,不會發生死鎖
常用函數
- 開發中比較常用的GCD函數
延遲執行
- 一段時間之后再執行任務
- 三種方法可以實現延遲執行
- 方法一:performSelector:withObject:afterDelay:
- 方法二:定時器NSTimer scheduledTimeWithTimeInterval:target:selector:userInfo:repeats:
- 方法三:GCD dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(3.0*NSEC_PER_SEC)),dispatch_get_main_queue),^{}
- 默認是主隊列 ,可以改為全局并發隊列
- 隊列決定代碼塊在哪個線程中調用
- 主隊列 - 主線程
- 并發隊列 - 子線程
- 好處:可以控制在哪個線程里面執行任務
- 延遲執行內部實現原理?
- A:先把任務提交到隊列,然后三秒之后再執行該任務(錯誤)
- B:先等三秒的時間,然后再把任務提交到隊列
- 正確答案:B
- 延遲執行就是延遲提交
一次性代碼
- 只會執行一次,一輩子只會執行一次
- dispatch_once(&onceToken,^{});
- 特點:
- 整個程序運行過程中,只會執行一次
- 它本身是線程安全的,不用考慮線程安全的問題
- 怎么做到永遠只會執行一次呢?
- 通過靜態變量的值來判斷的
- static dispatch_once_t onceToken;
- 判斷onceToken值是否等于0,不等于零就不執行了
- 通過靜態變量的值來判斷的
快速迭代
迭代 - 遞歸 - 遍歷 - 博客ios開發中實用技巧
-
遍歷
- for循環,開多條線程就可以提高效率
- GCD的快速迭代dispatch_apply(10,并發隊列,^(size_t index){})
- 參數一:遍歷多少次,size_t 類似NSInteger類型
- 參數二:對列,看到隊列就要想,block塊在哪個線程中調用
- 參數三:^(size_t ){}類似于for循環里面的i
-
for循環與GCD的快速迭代區別
- 快速迭代沒有順序,開始迭代的時候是按照順序開始的,誰先結束是不知道的,所以打印出的結果是沒有順序的,是并發執行的
- GCD快速迭代:會開啟多條子線程和主線程一起并發的執行任務
- 注意:隊列怎么傳?不能傳主隊列!不傳串行隊列,不會死鎖,不會開線程,執行方式和for循環一樣,串行執行的!傳并發隊列!!!
- 用快速迭代就用并發隊列!!!!
-
快速迭代的應用
- 剪切文件
- 獲得路徑下面的子路徑(打印的是文件的名稱,剪切的時候需要的是文件的全路徑)
文件管理者NSFileManager defaultManager]subpathsAtPath: - 遍歷數組(快速迭代)
- 獲得路徑下面的子路徑(打印的是文件的名稱,剪切的時候需要的是文件的全路徑)
- 剪切文件
dispatch_get_global_queue(0,0)
dispatch_apply(subpaths.count,queue,^(size_t){
獲得文件的名稱
獲得要剪切文件的全路徑(stringByAppendingPathComponent:該方法在拼接的時候會自動添加一個/)
拼接文件的目標路徑(toFullPath)
剪切文件
[NSFileManager defaultManager] moveItemAtpath:toPath:error:(第一個參數:要剪切的文件在哪里?第二參數:要剪切到哪里去,全路徑;第三個參數:)
})
柵欄函數
- 獲得隊列:并發
- dispatch_queue_create("",DISPATCH_QUEUE_CONCURRENT)
- 為什么不用全局并發隊列?
- 柵欄函數不能使用全局并發隊列
- 如果使用全局并發隊列,柵欄函數就不具備攔截功能,和普通的異步函數沒有任何區別
- 使用柵欄函數,必須要自己創建隊列
- 異步函數
- dispatch_async(queue,^{})
- 需求:真實開發中,任務和任務之間有依賴關系,有先后順序關系,要求執行123三個任務后,執行打印操作,再執行后面的456三個任務
- 柵欄函數
- dispatch_barrier_async()一般用異步函數
- 在柵欄函數內部執行打印操作
- 作用:
- 等之前的所有任務都執行完畢之后,執行柵欄函數中的任務,等我的任務執行完畢之后,在執行后面的任務
- 前面的和后面的任務都是并發執行的
GCD隊列組(掌握)
和柵欄函數的功能很類似
-
1.獲得隊列
- 全局并發隊列dispatch_get_global_queue()
異步函數dispatch_async(queue,^{})
需求,在最后攔住任務,當所有的任務執行完畢之后做一些事情!
隊列組(調度組)
-
2.創建隊列組dispatch_group_create()
- 監聽隊列里面所有任務的執行情況
-
3.異步函數dispatch_group_async()
- 第一個參數:隊列組
- 第二個參數:隊列
- 通過隊列組建立起隊列組和隊列以及任務之間的關系
4.攔截dispatch_group_notify(group,queue,^{打印操作})當隊列組中所有的任務都執行完畢之后,會調用該方法
-
隊列組是如何知道任務什么時候結束的呢?
- 以前是把dispatch_group_async(group,queue,^{})拆分成三個方法
- 1.創建隊列組
- 2.獲得對列
- 3.該方法后面的異步任務會被隊列組監聽dispatch_group_enter(group)
- 4.使用異步函數封裝任務
- dispatch_async(queue,^{任務1,在里面監聽,enter和leave配對使用dispatch_group_leave(group)})
- 5.攔截通知,隊列決定block塊在哪個線程中執行 dispatch_group_notify(group,queue,^{})
- 以前是把dispatch_group_async(group,queue,^{})拆分成三個方法
-
dispatch_group_async()和dispatch_async()的區別
- 相同點:
- 封裝任務
- 把任務添加到隊列
- 不同點
- 隊列組能夠監聽該任務的執行情況(開始|結束)
- 相同點:
-
隊列組的應用
- 應用場景:下載圖片1,圖片2,把兩張圖片合成為一張圖片
- 耗時操作,耗時操作放到子線程中執行
- 要開2~3條子線程
- 獲得一個并發隊列dispatch_get_global_queue(0,0)
- 合成圖片操作,有一個隱藏的依賴關系
- 隊列組
- 柵欄函數
- 創建隊列組dispatch_group_creat()
- 異步函數dispatch_group_async(group,queue^{
URL:URLWithString
把圖片的二進制數據下載到本地dataWithContentOfURL:
轉換格式imageWithData
定義圖片的屬性,強引用轉換好的圖片
}) - 合成圖片
- dispatch_group_notify(group,queue,^{
開啟圖形上下文UIGraphicsBeginImageContext()
畫圖1和2 drawInRect:
根據上下文得到圖片 UIGraphicsGetImageFromCurrentImageContext()
關閉上下文UIGraphicsEndImageContext()
顯示圖片(線程間通信)
dispatch_async(dispatch_get_main_queue(),^{
self.imageV.image = image;
})
})
- dispatch_group_notify(group,queue,^{
- 應用場景:下載圖片1,圖片2,把兩張圖片合成為一張圖片
補充
-
GCD的其他用法
- 使用函數的方法封裝任務dispatch_async_f()
- 第一個參數:隊列
- 第二個參數:函數要接收的參數NULL
- 第三個參數:dispatch_function_t
- void(*dispatch_function_t)(void *)
- (*dispatch_function_t) -- run把第一個小括號替換成函數名稱
- 第三個參數直接傳run就可以了
- 使用函數的方法封裝任務dispatch_async_f()
-
全局并發隊列與自己創建的并發隊列的區別
- 全局并發隊列在整個應用程序中本身是默認存在的,并且對應有高優先級、默認優先級、低優先級和后臺優先級一共四個并發隊列,我們只選擇其中的一個直接拿來用,而creat函數是實打實的從頭開始去創建一個隊列
- 在ios6.0之前,在GCD中凡是使用了create和retain的函數在最后都需要做一次release操作。而主隊列和全局并發隊列不需要我們手動release.當然了,在ios6.0之后GCD已經被納入到了ARC的內存管理范疇中,即便是使用retain或者create函數創建的對象也不再需要開發人員手動釋放,我們像對待普通OC對象一樣對待GCD就OK
- 在使用柵欄函數的時候,蘋果官方明確規定,柵欄函數只有在和使用create函數自己創建的并發隊列一起使用的時候才有效果(沒有給出具體原因)
- 其他區別涉及到XNU內核的系統級線程編程,不一一列舉
- 參考
- GCDAPI
- Libdispatch版本源碼