寫在前面
多線程在iOS
中有著舉足輕重的地位甥角,那么本篇文章就來帶你全面走進她.....
一、基本概念及原理
① 線程控妻、進程與隊列
①.1 線程的定義
線程是進程的基本執(zhí)行單元迂烁,一個進程的所有任務(wù)都在線程中執(zhí)行
- 進程想要執(zhí)行任務(wù)看尼,必須得有線程,
進程至少要有一條線程
-
程序啟動會默認開啟一條線程
盟步,這條線程被成為主線程
或UI線程
①.2 進程的定義
-
進程
是指在系統(tǒng)中正在運行的一個應(yīng)用程序藏斩,如微信、支付寶app都是一個進程 - 每個
進程
之間是獨立的却盘,每個進程均運行在其專用的且受保護的內(nèi)存空間內(nèi) - 通過“活動監(jiān)視器”可以查看mac系統(tǒng)中所開啟的線程
所以狰域,可以簡單的理解為:進程是線程的容器
媳拴,而線程用來執(zhí)行任務(wù)
.在iOS
中是單進程開發(fā),一個進程就是一個app
兆览,進程之間是相互獨立的屈溉,如支付寶、微信抬探、qq等子巾,這些都是屬于不同的進程.
①.3 進程與線程的關(guān)系和區(qū)別
- 地址空間:同一
進程
的線程
共享本進程的地址空間,而進程之間則是獨立的地址空間 - 資源擁有:同一
進程
內(nèi)的線程
共享本進程的資源如內(nèi)存、I/O、cpu等汁胆,但是進程
之間的資源是獨立的 - 兩個之間的關(guān)系就相當(dāng)于
工廠與流水線的關(guān)系
,工廠與工廠之間是相互獨立的仪搔,而工廠中的流水線是共享工廠的資源的,即進程
相當(dāng)于一個工廠蜻牢,線程
相當(dāng)于工廠中的一條流水線
- 一個
進程
崩潰后僻造,在保護模式下不會對其他進程
產(chǎn)生影響,但是一個線程
崩潰整個進程
都死掉孩饼,所以多進程
要比多線程
健壯 -
進程
切換時髓削,消耗的資源大、效率高.所以設(shè)計到頻繁的切換時镀娶,使用線程
要好于進程
.同樣如果要求同時進行并且又要共享某些變量的并發(fā)操作立膛,只能用線程
而不能用進程
- 執(zhí)行過程:每個獨立的
進程
有一個程序運行的入口、順序執(zhí)行序列和程序入口.但是線程
不能獨立執(zhí)行梯码,必須依存在應(yīng)用程序中宝泵,由應(yīng)用程序提供多個線程
執(zhí)行控制 -
線程
是處理器調(diào)度的基本單位,但進程
不是 - 線程沒有地址空間,線程包含在進程地址空間中
可能會覺得這些理論知識很抽象轩娶,百度出來一大堆但是都不好理解儿奶,看完下面的理解就全明白了
①.4 進程與線程的關(guān)系圖
可以把iOS系統(tǒng)
想象成商場
,進程
則是商場中的店鋪
鳄抒,線程
是店鋪雇傭的員工
:
- 進程之間的相互獨立
- 奶茶店看不到果汁店的賬目(訪問不了別的進程的內(nèi)存)
- 果汁店用不了奶茶店的波霸(進程之間的資源是獨立的)
- 進程至少要有一條線程
- 店鋪至少要有一個員工(進程至少有一個線程)
- 早上開店門的員工(相當(dāng)于主線程)
- 進程/線程崩潰的情況
- 奶茶店倒閉了并不會牽連果汁店倒閉(進程崩潰不會對其他進程產(chǎn)生影響)
- 奶茶店的收銀員不干了會導(dǎo)致奶茶店無法正常運作(線程崩潰導(dǎo)致進程癱瘓)
移動開發(fā)不一定是單進程處理的闯捎,android就是多進程處理的;而iOS采用沙盒機制许溅,這也是蘋果運行能夠流暢安全的一個主要原因
①.5 線程和runloop的關(guān)系
-
runloop與線程是一一對應(yīng)的
—— 一個runloop
對應(yīng)一個核心的線程
瓤鼻,為什么說是核心的,是因為runloop
是可以嵌套的贤重,但是核心的只能有一個茬祷,他們的關(guān)系保存在一個全局的字典里 -
runloop是來管理線程的
—— 當(dāng)線程的runloop
被開啟后,線程會在執(zhí)行完任務(wù)后進入休眠狀態(tài)并蝗,有了任務(wù)就會被喚醒去執(zhí)行任務(wù) -
runloop
在第一次獲取時被創(chuàng)建祭犯,在線程結(jié)束時被銷毀- 對于主線程來說秸妥,
runloop
在程序一啟動就默認創(chuàng)建好了 - 對于子線程來說,
runloop
是懶加載的 —— 只有當(dāng)我們使用的時候才會創(chuàng)建沃粗,所以在子線程用定時器要注意:確保子線程的runloop被創(chuàng)建粥惧,不然定時器不會回調(diào)
- 對于主線程來說秸妥,
①.6 影響任務(wù)執(zhí)行速度的因素
以下因素都會對任務(wù)的執(zhí)行速度造成影響:
-
cpu
的調(diào)度 - 線程的執(zhí)行速率
- 隊列情況
- 任務(wù)執(zhí)行的復(fù)雜度
- 任務(wù)的優(yōu)先級
② 多線程
②.1 多線程原理
- 對于
單核CPU
,同一時間陪每,CPU
只能處理一條線程影晓,即只有一條線程在工作(執(zhí)行) -
iOS
中的多線程同時執(zhí)行的本質(zhì)
是CPU
在多個任務(wù)之間進行快速的切換镰吵,由于CPU
調(diào)度線程的時間足夠快檩禾,就造成了多線程的“同時”執(zhí)行的效果.其中切換的時間間隔就是時間片
②.2 多線程意義
優(yōu)點
- 能適當(dāng)提高程序的執(zhí)行效率
- 能適當(dāng)提高資源的利用率(CPU、內(nèi)存)
- 線程上的任務(wù)執(zhí)行完成后疤祭,線程會自動銷毀
缺點
- 開啟線程需要占用一定的內(nèi)存空間(默認情況下盼产,每一個線程都占
512KB
,創(chuàng)建線程大約需要90毫秒
的創(chuàng)建時間) - 如果開啟大量的線程勺馆,會占用大量的內(nèi)存空間戏售,降低程序的性能
- 線程越多,
CPU
在調(diào)用線程上的開銷就越大 - 程序設(shè)計更加復(fù)雜草穆,比如線程間的通信灌灾、多線程的數(shù)據(jù)共享
②.3 多線程生命周期
多線程的生命周期主要分為5部分:新建 - 就緒 - 運行 - 阻塞 - 死亡,如下圖所示
-
新建
:主要是實例化線程對象 -
就緒
:線程對象調(diào)用start
方法悲柱,將線程對象加入可調(diào)度線程池
锋喜,等待CPU的調(diào)用
,即調(diào)用start
方法豌鸡,并不會立即執(zhí)行嘿般,進入就緒狀態(tài)
,需要等待一段時間涯冠,經(jīng)CPU
調(diào)度后才執(zhí)行炉奴,也就是從就緒狀態(tài)進入運行狀態(tài)
-
運行
:CPU
負責(zé)調(diào)度可調(diào)度線程池中線程的執(zhí)行.在線程執(zhí)行完成之前,其狀態(tài)可能會在就緒和運行之間來回切換.就緒和運行之間的狀態(tài)變化由CPU負責(zé)蛇更,程序員不能干預(yù). -
阻塞
:當(dāng)滿足某個預(yù)定條件時瞻赶,可以使用休眠或鎖
,阻塞線程執(zhí)行.sleepForTimeInterval
(休眠指定時長)派任,sleepUntilDate
(休眠到指定日期)共耍,@synchronized(self)
:(互斥鎖) -
死亡
:分為兩種情況:正常死亡,即線程執(zhí)行完畢. 非正常死亡吨瞎,即當(dāng)滿足某個條件后痹兜,在線程內(nèi)部(或者主線程中)終止執(zhí)行(調(diào)用exit方法等退出)
簡要說明,就是處于運行中的線程
擁有一段可以執(zhí)行的時間(稱為時間片
)
- 如果
時間片用盡
颤诀,線程就會進入就緒狀態(tài)隊列
- 如果
時間片沒有用盡
字旭,且需要開始等待某事件
对湃,就會進入阻塞狀態(tài)隊列
- 等待事件發(fā)生后,線程又會重新進入
就緒狀態(tài)隊列
- 每當(dāng)一個
線程離開運行
遗淳,即執(zhí)行完畢或者強制退出后拍柒,會重新從就緒狀態(tài)隊列
中選擇一個線程繼續(xù)執(zhí)行
線程的exit
和cancel
說明
-
exit
:一旦強行終止線程,后續(xù)的所有代碼都不會執(zhí)行 -
cancel
:取消當(dāng)前線程屈暗,但是不能取消正在執(zhí)行的線程
那么是不是線程的優(yōu)先級越高拆讯,意味著任務(wù)的執(zhí)行越快?
并不是养叛,線程執(zhí)行的快慢种呐,除了要看優(yōu)先級,還需要查看資源的大小
(即任務(wù)的復(fù)雜度)弃甥、以及 CPU 調(diào)度
情況.在NSThread
中爽室,線程優(yōu)先級threadPriority
已經(jīng)被服務(wù)質(zhì)量qualityOfService
取代,以下是相關(guān)的枚舉值
②.4 線程池的原理
-
【第一步】判斷核心線程池是否都正在執(zhí)行任務(wù)
- 返回NO淆攻,創(chuàng)建新的工作線程去執(zhí)行
- 返回YES阔墩,進入【第二步】
-
【第二步】判斷線程池工作隊列是否已經(jīng)飽滿
- 返回NO,將任務(wù)存儲到工作隊列瓶珊,等待CPU調(diào)度
- 返回YES啸箫,進入【第三步】
-
【第三步】判斷線程池中的線程是否都處于執(zhí)行狀態(tài)
- 返回NO,安排可調(diào)度線程池中空閑的線程去執(zhí)行任務(wù)
- 返回YES伞芹,進入【第四步】
-
【第四步】交給飽和策略去執(zhí)行忘苛,主要有以下四種(在iOS中并沒有找到以下4種策略)
-
AbortPolicy
:直接拋出RejectedExecutionExeception
異常來阻止系統(tǒng)正常運行 -
CallerRunsPolicy
:將任務(wù)回退到調(diào)用者 -
DisOldestPolicy
:丟掉等待最久的任務(wù) -
DisCardPolicy
:直接丟棄任務(wù)
-
②.5 iOS中多線程的實現(xiàn)方案
iOS中的多線程實現(xiàn)方式,主要有四種:pthread丑瞧、NSThread柑土、GCD、NSOperation
绊汹,匯總?cè)鐖D所示
C和OC的橋接
其中涉及C與OC的橋接稽屏,有以下幾點說明
-
__bridge
只做類型轉(zhuǎn)換,但是不修改對象(內(nèi)存)管理權(quán)
-
__bridge_retained
(也可以使用CFBridgingRetain
)將Objective-C
的對象轉(zhuǎn)換為Core Foundation
的對象西乖,同時將對象(內(nèi)存)的管理權(quán)交給我們
狐榔,后續(xù)需要使用CFRelease
或者相關(guān)方法來釋放對象
-
__bridge_transfer
(也可以使用CFBridgingRelease
)將Core Foundation
的對象轉(zhuǎn)換為Objective-C
的對象,同時將對象(內(nèi)存)的管理權(quán)交給ARC
②.6 線程安全問題
當(dāng)多個線程同時訪問一塊資源時获雕,容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全問題薄腻,有以下兩種解決方案
- 互斥鎖(即同步鎖):
@synchronized
- 自旋鎖
②.6.1 互斥鎖 vs 自旋鎖
互斥鎖
- 保證鎖內(nèi)的代碼,同一時間届案,只有一條線程能夠執(zhí)行!
- 互斥鎖的鎖定范圍庵楷,應(yīng)該盡量小,鎖定范圍越大,效率越差!
- 加了互斥鎖的代碼尽纽,當(dāng)新線程訪問時咐蚯,如果發(fā)現(xiàn)其他線程正在執(zhí)行鎖定的代碼,新線程就會進入休眠
- 能夠加鎖的任意
NSObject
對象 - 注意:鎖對象一定要保證所有的線程都能夠訪問
- 如果代碼中只有一個地方需要加鎖弄贿,大多都使用
self
春锋,這樣可以避免單獨再創(chuàng)建一個鎖對象
自旋鎖
- 自旋鎖與互斥鎖類似,但它不是通過休眠使線程阻塞差凹,而是在獲取鎖之前一直處于
忙等
(即原地打轉(zhuǎn)期奔,稱為自旋)阻塞狀態(tài) - 使用場景:鎖持有的時間短,且線程不希望在重新調(diào)度上花太多成本時危尿,就需要使用自旋鎖呐萌,屬性修飾符
atomic
,本身就有一把自旋鎖
- 加入了自旋鎖脚线,當(dāng)新線程訪問代碼時搁胆,如果發(fā)現(xiàn)有其他線程正在鎖定代碼弥搞,新線程會用
死循環(huán)
的方法邮绿,一直等待鎖定的代碼執(zhí)行完成,即不停的嘗試執(zhí)行代碼攀例,比較消耗性能
考考你: 自旋鎖vs互斥鎖的區(qū)別?
相同點:在同一時間船逮,保證了只有一條線程執(zhí)行任務(wù),即保證了相應(yīng)同步的功能
-
不同點:
-
互斥鎖
:發(fā)現(xiàn)其他線程執(zhí)行粤铭,當(dāng)前線程休眠
(即就緒狀態(tài)
)挖胃,進入等待執(zhí)行,即掛起.一直等其他線程打開之后梆惯,然后喚醒執(zhí)行 -
自旋鎖
:發(fā)現(xiàn)其他線程執(zhí)行酱鸭,當(dāng)前線程忙等
(即一直訪問),處于忙等狀態(tài)垛吗,耗費的性能比較高
-
-
使用場景:根據(jù)任務(wù)復(fù)雜度區(qū)分凹髓,使用不同的鎖
- 當(dāng)前的任務(wù)狀態(tài)比較
短小精悍
時,用自旋鎖
- 反之的怯屉,用
互斥鎖
- 當(dāng)前的任務(wù)狀態(tài)比較
②.6.2 atomic與nonatomic 的區(qū)別
atomic
和 nonatomic
主要用于屬性的修飾蔚舀,以下是相關(guān)的一些說明
-
nonatomic
非原子屬性 -
atomic
原子屬性(線程安全),針對多線程設(shè)計的锨络,默認值- 保證同一時間只有一個線程能夠?qū)懭?但是同一個時間多個線程都可以取值)
-
atomic
本身就有一把鎖(自旋鎖
) - 單寫多讀:單個線程寫入赌躺,多個線程可以讀取
-
atomic
:線程安全,需要消耗大量的資源 -
nonatomic
:非線程安全羡儿,適合內(nèi)存小的移動設(shè)備
iOS
開發(fā)的建議
- 所有屬性都聲明為
nonatomic
- 盡量避免多線程搶奪同一塊資源 盡量將加鎖礼患、資源搶奪的業(yè)務(wù)邏輯交給服務(wù)器端處理,減小移動客戶端的壓力
②.7 線程間通訊
在Threading Programming Guide文檔中,提及缅叠,線程間的通訊有以下幾種方式
[圖片上傳失敗...(image-a8fb76-1614613062037)]
-
直接消息傳遞
: 通過performSelector
的一系列方法咏瑟,可以實現(xiàn)由某一線程指定在另外的線程上執(zhí)行任務(wù).因為任務(wù)的執(zhí)行上下文是目標(biāo)線程,這種方式發(fā)送的消息將會自動的被序列化 -
全局變量痪署、共享內(nèi)存塊和對象
: 在兩個線程之間傳遞信息的另一種簡單方法是使用全局變量码泞,共享對象或共享內(nèi)存塊.盡管共享變量既快速又簡單,但是它們比直接消息傳遞更脆弱.必須使用鎖或其他同步機制仔細保護共享變量狼犯,以確保代碼的正確性. 否則可能會導(dǎo)致競爭狀況余寥,數(shù)據(jù)損壞或崩潰。 -
條件執(zhí)行
: 條件是一種同步工具
悯森,可用于控制線程何時執(zhí)行代碼的特定部分.您可以將條件視為關(guān)守欣喧,讓線程僅在滿足指定條件時運行. -
Runloop sources
: 一個自定義的Runloop source
配置可以讓一個線程上收到特定的應(yīng)用程序消息.由于Runloop source
是事件驅(qū)動的谬运,因此在無事可做時,線程會自動進入睡眠狀態(tài)
,從而提高了線程的效率 -
Ports and sockets
:基于端口的通信
是在兩個線程之間進行通信的一種更為復(fù)雜的方法遥赚,但它也是一種非常可靠的技術(shù).更重要的是狂秦,端口和套接字可用于與外部實體(例如其他進程和服務(wù))進行通信.為了提高效率关带,使用Runloop source
來實現(xiàn)端口,因此當(dāng)端口上沒有數(shù)據(jù)等待時褥傍,線程將進入睡眠狀態(tài).需要注意的是儡嘶,端口通訊需要將端口加入到主線程的Runloop中
,否則不會走到端口回調(diào)方法 -
消息隊列
: 傳統(tǒng)的多處理服務(wù)定義了先進先出(FIFO)
隊列抽象恍风,用于管理傳入和傳出數(shù)據(jù).盡管消息隊列既簡單又方便蹦狂,但是它們不如其他一些通信技術(shù)高效 -
Cocoa 分布式對象
: 分布式對象是一種Cocoa
技術(shù),可提供基于端口的通信的高級實.盡管可以將這種技術(shù)用于線程間通信朋贬,但是強烈建議不要這樣做凯楔,因為它會產(chǎn)生大量開銷.分布式對象更適合與其他進程進行通信,盡管在這些進程之間進行事務(wù)的開銷也很高.
②.8 GCD和NSOperation的比較
-
GCD
和NSOperation
的關(guān)系如下:-
GCD
是面向底層的C
語言的API
-
NSOperation
是用GCD
封裝構(gòu)建的锦募,是GCD
的高級抽象
-
-
GCD和NSOperation的對比如下:
-
GCD
執(zhí)行效率更高摆屯,而且由于隊列中執(zhí)行的是由block
構(gòu)成的任務(wù),這是一個輕量級的數(shù)據(jù)結(jié)構(gòu) —— 寫起來更加方便 -
GCD
只支持FIFO
的隊列御滩,而NSOpration
可以設(shè)置最大并發(fā)數(shù)鸥拧、設(shè)置優(yōu)先級、添加依賴關(guān)系等調(diào)整執(zhí)行順序 -
NSOpration
甚至可以跨隊列設(shè)置依賴關(guān)系削解,但是GCD
只能通過設(shè)置串行隊列富弦,或者在隊列內(nèi)添加barrier
任務(wù)才能控制執(zhí)行順序,較為復(fù)雜 -
NSOperation
支持KVO
(面向?qū)ο螅┛梢詸z測operation
是否正在執(zhí)行氛驮、是否結(jié)束腕柜、是否取消
-
二、NSthread
NSthread
是蘋果官方提供面向?qū)ο蟮木€程操作技術(shù),是對thread
的上層封裝盏缤,比較偏向于底層.簡單方便砰蠢,可以直接操作線程對象,使用頻率較少.
① 創(chuàng)建線程
線程的創(chuàng)建方式主要有以下三種方式
- 通過
init
初始化方式創(chuàng)建 - 通過
detachNewThreadSelector
構(gòu)造器方式創(chuàng)建 - 通過
performSelector...
方法創(chuàng)建唉铜,主要是用于獲取主線程
台舱,以及后臺線程
② 屬性
③ 類方法
常用的類方法有以下幾個
-
currentThread
:獲取當(dāng)前線程 -
sleep...
:阻塞線程 -
exit
:退出線程 -
mainThread
:獲取主線程
三、GCD
① GCD簡介
什么是GCD
?
GCD
全稱是Grand Central Dispatch
潭流,它是純 C
語言竞惋,并且提供了非常多強大的函數(shù)
GCD
的優(yōu)勢:
-
GCD
是蘋果公司為多核的并行運算
提出的解決方案 -
GCD
會自動利用
更多的CPU內(nèi)核
(比如雙核、四核) -
GCD
會自動管理
線程的生命周期(創(chuàng)建線程灰嫉、調(diào)度任務(wù)拆宛、銷毀線程) - 程序員只需要告訴
GCD
想要執(zhí)行什么任務(wù),不需要編寫任何線程管理代碼
用一句話總結(jié)GCD就是:將任務(wù)添加到隊列讼撒,并且指定執(zhí)行任務(wù)的函數(shù)
② GCD核心
在日常開發(fā)中浑厚,GCD
一般寫成下面這種形式
將上述代碼拆分,方便我們來理解GCD
的核心,主要是由 任務(wù) + 隊列 + 函數(shù)
構(gòu)成
- 使用
dispatch_block_t
創(chuàng)建任務(wù) - 使用
dispatch_queue_t
創(chuàng)建隊列 - 將任務(wù)添加到隊列根盒,并指定執(zhí)行任務(wù)的函數(shù)
dispatch_async
注意
這里的任務(wù)
是指執(zhí)行操作
的意思钳幅,在使用dispatch_block_t
創(chuàng)建任務(wù)時,主要有以下兩點說明
- 任務(wù)使用
block
封裝 - 任務(wù)的
block
沒有參數(shù)也沒有返回值
③ 函數(shù)與隊列
③.1 函數(shù)
在GCD
中執(zhí)行任務(wù)的方式有兩種郑象,同步執(zhí)行
和異步執(zhí)行
贡这,分別對應(yīng)同步函數(shù)dispatch_sync
和 異步函數(shù)dispatch_async
,兩者對比如下
-
同步執(zhí)行
茬末,對應(yīng)同步函數(shù)dispatch_sync
- 必須等待當(dāng)前語句執(zhí)行完畢厂榛,才會執(zhí)行下一條語句
-
不會開啟線程
,即不具備開啟新線程的能力 - 在當(dāng)前線程中執(zhí)行
block
任務(wù)
-
異步執(zhí)行
丽惭,對應(yīng)異步函數(shù)dispatch_async
- 不用等待當(dāng)前語句執(zhí)行完畢击奶,就可以執(zhí)行下一條語句
-
會開啟線程
執(zhí)行block
任務(wù),即具備開啟新線程的能力(但并不一定開啟新線程责掏,這個與任務(wù)所指定的隊列類型有關(guān)) - 異步是多線程的代名詞
綜上所述柜砾,兩種執(zhí)行方式的主要區(qū)別
有兩點:
-
是否等待
隊列的任務(wù)執(zhí)行完畢 -
是否具備開啟新線程
的能力
③.2 隊列
多線程中所說的隊列
(Dispatch Queue
)是指執(zhí)行任務(wù)的等待隊列
,即用來存放任務(wù)的隊列.隊列是一種特殊的線性表
换衬,遵循先進先出(FIFO)
原則痰驱,即新任務(wù)總是被插入到隊尾,而任務(wù)的讀取從隊首開始讀取.每讀取一個任務(wù)瞳浦,則動隊列中釋放一個任務(wù)担映,如下圖所示
③.2.1 串行隊列 和 并發(fā)隊列
在GCD
中,隊列主要分為串行隊列(Serial Dispatch Queue)
和并發(fā)隊列(Concurrent Dispatch Queue)
兩種叫潦,如下圖所示
-
串行隊列
:每次只有一個任務(wù)被執(zhí)行
蝇完,等待上一個任務(wù)執(zhí)行完畢再執(zhí)行下一個,即只開啟一個線程
(通俗理解:同一時刻只調(diào)度一個任務(wù)執(zhí)行)- 使用
dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL);
創(chuàng)建串行隊列 - 其中的
DISPATCH_QUEUE_SERIAL
也可以使用NULL
表示,這兩種均表示默認的串行隊列
- 使用
-
并發(fā)隊列
:一次可以并發(fā)執(zhí)行多個任務(wù)
短蜕,即開啟多個線程
氢架,并同時執(zhí)行任務(wù)(通俗理解:同一時刻可以調(diào)度多個任務(wù)執(zhí)行)- 使用
dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);
創(chuàng)建并發(fā)隊列 - 注意:并發(fā)隊列的并發(fā)功能只有在
異步函數(shù)
下才有效
- 使用
③.2.2 主隊列 和 全局并發(fā)隊列
在GCD
中,針對上述兩種隊列朋魔,分別提供了主隊列(Main Dispatch Queue)
和全局并發(fā)隊列(Global Dispatch Queue)
-
主隊列
(Main Dispatch Queue
):GCD
中提供的特殊的串行隊列
- 專門用來
在主線程上調(diào)度任務(wù)的串行隊列
岖研,依賴于主線程
、主Runloop
警检,在main函數(shù)
調(diào)用之前自動創(chuàng)建
- 不會開啟線程
- 如果當(dāng)前主線程正在有任務(wù)執(zhí)行缎玫,那么無論主隊列中當(dāng)前被添加了什么任務(wù),都不會被調(diào)度
- 使用
dispatch_get_main_queue()
獲得主隊列 - 通常在返回
主線程更新UI
時使用
- 專門用來
-
全局并發(fā)隊列
(Global Dispatch Queue
):GCD
提供的默認的并發(fā)隊列
- 為了方便程序員的使用解滓,蘋果提供了全局隊列
- 在使用多線程開發(fā)時赃磨,如果對隊列沒有特殊需求,在執(zhí)行
異步
任務(wù)時洼裤,可以直接使用全局隊列 - 使用
dispatch_get_global_queue
獲取全局并發(fā)隊列邻辉,最簡單的是dispatch_get_global_queue(0, 0)
- 第一個參數(shù)表示
隊列優(yōu)先級
,默認優(yōu)先級為DISPATCH_QUEUE_PRIORITY_DEFAULT=0
腮鞍,在ios9
之后值骇,已經(jīng)被服務(wù)質(zhì)量(quality-of-service)
取代 -
第二個參數(shù)使用0
- 第一個參數(shù)表示
③.2.3 全局并發(fā)隊列 + 主隊列 配合使用
在日常開發(fā)中,全局隊列+并發(fā)并列
一般是這樣配合使用的
③.3 函數(shù)與隊列的不同組合
主隊列和全局隊列單獨考慮移国,組合結(jié)果以總結(jié)表格為準(zhǔn)
③.3.1 串行隊列 + 同步函數(shù)
任務(wù)一個接一個的在當(dāng)前線程執(zhí)行吱瘩,不會開辟新線程
③.3.2 串行隊列 + 異步函數(shù)
任務(wù)一個接一個的執(zhí)行,會開辟新線程
③.3.3 并發(fā)隊列 + 同步函數(shù)
任務(wù)一個接一個的執(zhí)行迹缀,不開辟線程③.3.4 并發(fā)隊列 + 異步函數(shù)
任務(wù)亂序
執(zhí)行使碾,會開辟新線程
③.3.5 主隊列 + 同步函數(shù)
任務(wù)相互等待
,造成死鎖
造成死鎖的原因分析如下:
- 主隊列有兩個任務(wù)祝懂,順序為:
CJNSLog任務(wù)
-同步block
- 執(zhí)行
CJNSLog
任務(wù)后票摇,執(zhí)行同步Block
,會將任務(wù)1(即i=1時)加入到主隊列砚蓬,主隊列順序為:CJNSLog任務(wù) - 同步block - 任務(wù)1
-
任務(wù)1
的執(zhí)行需要等待同步block執(zhí)行完畢
才會執(zhí)行矢门,而同步block的執(zhí)行
需要等待任務(wù)1執(zhí)行完畢
,所以就造成了任務(wù)互相等待
的情況灰蛙,即造成死鎖崩潰
死鎖現(xiàn)象
-
主線程
因為你同步函數(shù)
的原因等著先執(zhí)行任務(wù) -
主隊列
等著主線程的任務(wù)執(zhí)行完畢再執(zhí)行自己的任務(wù) -
主隊列和主線程
相互等待會造成死鎖
③.3.6 主隊列 + 異步函數(shù)
任務(wù)一個接一個的執(zhí)行祟剔,不開辟線程
③.3.7 全局并發(fā)隊列 + 同步函數(shù)
任務(wù)一個接一個的執(zhí)行,不開辟新線程
③.3.8 全局并發(fā)隊列 + 異步函數(shù)
任務(wù)亂序
執(zhí)行摩梧,會開辟新線程
③.3.9 總結(jié)
函數(shù)與隊列 | 串行隊列 | 并發(fā)隊列 | 主隊列 | 全局并發(fā)隊列 |
---|---|---|---|---|
同步函數(shù) | 順序執(zhí)行,不開辟線程 | 順序執(zhí)行,不開辟線程 | 死鎖 | 順序執(zhí)行,不開辟線程 |
異步函數(shù) | 順序執(zhí)行,開辟線程 | 亂序執(zhí)行,開辟線程 | 順序執(zhí)行,不開辟線程 | 亂序執(zhí)行,開辟線程 |
④ dispatch_after
⑤ dispatch_once
⑥ dispatch_apply
⑦ dispatch_group_t
dispatch_group_t:調(diào)度組將任務(wù)分組執(zhí)行物延,能監(jiān)聽任務(wù)組完成,并設(shè)置等待時間
應(yīng)用場景:多個接口請求之后刷新頁面
有以下兩種使用方式
⑦.1 使用dispatch_group_async + dispatch_group_notify
dispatch_group_notify
在dispatch_group_async
執(zhí)行結(jié)束之后會受收到通知
⑦.2 使用dispatch_group_enter + dispatch_group_leave + dispatch_group_notify
dispatch_group_enter
和dispatch_group_leave
成對出現(xiàn)障本,使進出組的邏輯更加清晰
調(diào)度組要注意搭配使用教届,必須先進組再出組响鹃,缺一不可
⑦.3 在⑦.2 的基礎(chǔ)上使用 dispatch_group_wait
⑧ dispatch_barrier_sync & dispatch_barrier_async
柵欄函數(shù),主要有兩種使用場景:串行隊列案训、并發(fā)隊列.
應(yīng)用場景:同步鎖
等柵欄前追加到隊列中的任務(wù)執(zhí)行完畢后买置,再將柵欄后的任務(wù)追加到隊列中.
簡而言之,就是先執(zhí)行柵欄前任務(wù)强霎,再執(zhí)行柵欄任務(wù)忿项,最后執(zhí)行柵欄后任務(wù).
⑧.1 串行隊列使用柵欄函數(shù)
不使用柵欄函數(shù)
使用柵欄函數(shù)
柵欄函數(shù)的作用是將隊列中的任務(wù)進行分組,所以我們只要關(guān)注任務(wù)1
城舞、任務(wù)2
結(jié)論:由于串行隊列異步執(zhí)行
任務(wù)是一個接一個執(zhí)行完畢的轩触,所以使用柵欄函數(shù)沒意義
⑧.2 并發(fā)隊列使用柵欄函數(shù)
不使用柵欄函數(shù)
使用柵欄函數(shù)
結(jié)論:由于并發(fā)隊列異步執(zhí)行
任務(wù)是亂序執(zhí)行完畢的,所以使用柵欄函數(shù)可以很好的控制隊列內(nèi)任務(wù)執(zhí)行的順序
⑧.3 dispatch_barrier_sync/dispatch_barrier_async區(qū)別
-
dispatch_barrier_async
:前面的任務(wù)執(zhí)行完畢才會來到這里 -
dispatch_barrier_sync
:作用相同家夺,但是這個會堵塞線程脱柱,影響后面的任務(wù)執(zhí)行
將案例二中的dispatch_barrier_async
改成dispatch_barrier_sync
結(jié)論:dispatch_barrier_async可以控制隊列中任務(wù)的執(zhí)行順序,而dispatch_barrier_sync不僅阻塞了隊列的執(zhí)行拉馋,也阻塞了線程的執(zhí)行(盡量少用)
⑧.4 柵欄函數(shù)注意點
- 1.
盡量使用自定義的并發(fā)隊列
:- 使用
全局隊列
起不到柵欄函數(shù)
的作用 - 使用
全局隊列
時由于對全局隊列造成堵塞榨为,可能致使系統(tǒng)其他調(diào)用全局隊列的地方也堵塞從而導(dǎo)致崩潰(并不是只有你在使用這個隊列)
- 使用
- 2.
柵欄函數(shù)只能控制同一并發(fā)隊列
:打個比方,平時在使用AFNetworking
做網(wǎng)絡(luò)請求時為什么不能用柵欄函數(shù)起到同步鎖堵塞的效果煌茴,因為AFNetworking
內(nèi)部有自己的隊列
⑨ dispatch_semaphore_t
信號量主要用作同步鎖
随闺,用于控制GCD
最大并發(fā)數(shù)
-
dispatch_semaphore_create()
:創(chuàng)建信號量 -
dispatch_semaphore_wait()
:等待信號量,信號量減1
.當(dāng)信號量< 0
時會阻塞當(dāng)前線程蔓腐,根據(jù)傳入的等待時間決定接下來的操作——如果永久等待將等到信號(signal)
才執(zhí)行下去 -
dispatch_semaphore_signal()
:釋放信號量矩乐,信號量加1
.當(dāng)信號量>= 0
會執(zhí)行wait
之后的代碼.
下面這段代碼要求使用信號量來按序輸出(當(dāng)然柵欄函數(shù)可以滿足要求)
利用信號量的API
來進行代碼改寫
如果當(dāng)創(chuàng)建信號量時傳入值為1又會怎么樣呢?
-
i=0
時有可能先打印回论,也可能會先發(fā)出wait
信號量-1散罕,但是wait
之后信號量為0不會阻塞線程,所以進入i=1
-
i=1
時有可能先打印透葛,也可能會先發(fā)出wait
信號量-1笨使,但是wait
之后信號量為-1阻塞線程,等待signal
再執(zhí)行下去
結(jié)論:
- 創(chuàng)建信號量時傳入值為1時僚害,可以通過兩次才堵塞
- 傳入值為2時,可以通過三次才堵塞
⑩ dispatch_source
dispatch_source_t
主要用于計時操作繁调,其原因是因為它創(chuàng)建的timer
不依賴于RunLoop
萨蚕,且計時精準(zhǔn)度比NSTimer
高
⑩.1 定義及使用
dispatch_source
是一種基本的數(shù)據(jù)類型,可以用來監(jiān)聽一些底層的系統(tǒng)事件
-
Timer Dispatch Source
:定時器事件源蹄胰,用來生成周期性的通知或回調(diào) -
Signal Dispatch Source
:監(jiān)聽信號事件源岳遥,當(dāng)有UNIX信號發(fā)生時會通知 -
Descriptor Dispatch Source
:監(jiān)聽文件或socket
事件源,當(dāng)文件或socket
數(shù)據(jù)發(fā)生變化時會通知 -
Process Dispatch Source
:監(jiān)聽進程事件源裕寨,與進程相關(guān)的事件通知 -
Mach port Dispatch Source
:監(jiān)聽Mach
端口事件源 -
Custom Dispatch Source
:監(jiān)聽自定義事件源
主要使用的API:
-
dispatch_source_create
: 創(chuàng)建事件源 -
dispatch_source_set_event_handler
: 設(shè)置數(shù)據(jù)源回調(diào) -
dispatch_source_merge_data
: 設(shè)置事件源數(shù)據(jù) -
dispatch_source_get_data
: 獲取事件源數(shù)據(jù) -
dispatch_resume
: 繼續(xù) -
dispatch_suspend
: 掛起 -
dispatch_cancle
: 取消
⑩.2 自定義定時器
在iOS開發(fā)中一般使用NSTimer
來處理定時邏輯浩蓉,但NSTimer
是依賴Runloop
的派继,而Runloop
可以運行在不同的模式下.如果NSTimer
添加在一種模式下,當(dāng)Runloop
運行在其他模式下的時候捻艳,定時器就掛機了驾窟;又如果Runloop
在阻塞狀態(tài),NSTimer
觸發(fā)時間就會推遲到下一個Runloop
周.认轨。因此NSTimer
在計時上會有誤差绅络,并不是特別精確,而GCD定時器
不依賴Runloop
嘁字,計時精度要高很多
使用dispatch_source
自定義定時器注意點:
-
GCDTimer
需要強持有
恩急,否則出了作用域立即釋放,也就沒有了事件回調(diào) -
GCDTimer
默認是掛起狀態(tài)纪蜒,需要手動激活 -
GCDTimer
沒有repeat
衷恭,需要封裝來增加標(biāo)志位控制 -
GCDTimer
如果存在循環(huán)引用,使用weak+strong
或者提前調(diào)用dispatch_source_cancel
取消timer
-
dispatch_resume
和dispatch_suspend
調(diào)用次數(shù)需要平衡 -
source
在掛起狀態(tài)
下纯续,如果直接設(shè)置source = nil
或者重新創(chuàng)建source
都會造成crash
.正確的方式是在激活狀態(tài)下調(diào)用dispatch_source_cancel(source)
釋放當(dāng)前的source
四匾荆、NSOperation
NSOperation
是個抽象類,依賴于子類NSInvocationOperation
杆烁、NSBlockOperation
去實現(xiàn)
下面是開發(fā)者文檔上對NSOperation
的一段描述
① NSInvocationOperation
-
基本使用
-
直接處理事務(wù)牙丽,不添加隱性隊列
-
接下來就會引申出下面一段錯誤使用代碼
上述代碼之所以會崩潰厚掷,是因為線程生命周期:
-
queue addOperation:op
已經(jīng)將處理事務(wù)的操作任務(wù)加入到隊列中续誉,并讓線程運行 -
op start
將已經(jīng)運行的線程再次運行會造成線程混亂
② NSBlockOperation
NSInvocationOperation
和NSBlockOperation
兩者的區(qū)別在于:
- 前者類似
target
形式 - 后者類似
block
形式——函數(shù)式編程,業(yè)務(wù)邏輯代碼可讀性更高
NSOperationQueue
是異步執(zhí)行的锐墙,所以任務(wù)一
析校、任務(wù)二
的完成順序不確定
通過addExecutionBlock
這個方法可以讓NSBlockOperation
實現(xiàn)多線程
③ 自定義繼承自NSOperation的子類构罗,通過實現(xiàn)內(nèi)部相應(yīng)的方法來封裝任務(wù)
④ NSOperationQueue
NSOperationQueue
有兩種隊列:主隊列、其他隊列.其他隊列包含了 串行和并發(fā)
.
- 主隊列:主隊列上的任務(wù)是在主線程執(zhí)行的
- 其他隊列(非主隊列):加入到
非主隊列
中的任務(wù)默認就是并發(fā)智玻,開啟多線程
例如我們在② NSBlockOperation
中說的那樣.
⑤ 執(zhí)行順序
下列代碼可以證明操作與隊列的執(zhí)行效果是異步并發(fā)
的
⑥ 設(shè)置優(yōu)先級
NSOperation
設(shè)置優(yōu)先級只會讓CPU
有更高的幾率調(diào)用遂唧,不是說設(shè)置高就一定全部先完成
-
不使用
sleep
——高優(yōu)先級的任務(wù)一
先于低優(yōu)先級的任務(wù)二
-
使用
sleep
進行延時——高優(yōu)先級的任務(wù)一
慢于低優(yōu)先級的任務(wù)二
⑦ 設(shè)置并發(fā)數(shù)
- 在
GCD
中只能使用信號量來設(shè)置并發(fā)數(shù) - 而
NSOperation
輕易就能設(shè)置并發(fā)數(shù)- 通過設(shè)置
maxConcurrentOperationCount
來控制單次出隊列去執(zhí)行的任務(wù)數(shù)
- 通過設(shè)置
⑧ 添加依賴
在NSOperation
中添加依賴能很好的控制任務(wù)執(zhí)行的先后順序
⑨ 線程間通訊
- 在
GCD
中使用異步進行網(wǎng)絡(luò)請求,然后回到主線程刷新UI
-
NSOperation
中也有類似在線程間通訊的操作
⑩ 任務(wù)的掛起吊奢、繼續(xù)盖彭、取消
這幅圖是并發(fā)量為2的情況:
- 掛起前:
任務(wù)3
页滚、任務(wù)4
等待被調(diào)度 - 掛起瞬間:
任務(wù)3
召边、任務(wù)4
已經(jīng)被調(diào)度出隊列,準(zhǔn)備執(zhí)行裹驰,此時它們是無法掛起的 - 掛起后:
任務(wù)3
隧熙、任務(wù)4
被線程執(zhí)行,而原來的隊列被掛起不能被調(diào)度
五幻林、GCD底層分析
由于源碼的篇幅較大贞盯、邏輯分支音念、宏定義較多,使得源碼變得晦澀難懂躏敢,讓開發(fā)者們望而卻步.但如果帶著疑問闷愤、有目的性的去看源碼,就能減少難度父丰,忽略無關(guān)的代碼.首先提出我們要分析的幾個問題:
- 隊列創(chuàng)建
- 異步函數(shù)
- 同步函數(shù)
- 單例的原理
- 柵欄函數(shù)的原理
- 信號量的原理
- 調(diào)度組的原理
① 源碼的出處
分析源碼首先得獲取到GCD
源碼肝谭,之前已經(jīng)分析過objc、malloc蛾扇、dyld源碼攘烛,那么GCD
內(nèi)容是在哪份源碼中呢?
這里分享一個小技巧镀首,由于已知要研究GCD
坟漱,所以有以下幾種選擇源碼的方法
- Baidu/Google
- 下符號斷點
dispatch_queue_create
或dispatch_async
,打開匯編調(diào)式Debug->Debug Workflow->Always show Disassembly
這樣子就找到了我們需要的libdispatch-1271.40.12源碼
② 隊列創(chuàng)建
通過前面的學(xué)習(xí)我們知道隊列的創(chuàng)建是通過GCD
中的dispatch_queue_create
方法創(chuàng)建的,因此可以在源碼中搜索dispatch_queue_create
.
假如我們就直接搜索dispatch_queue_create
的話,會出現(xiàn)眾多的情況(66 results in 18 files),這時候就考驗一個開發(fā)者閱讀源碼的經(jīng)驗了
在此,我們就要改一改搜索條件了:
- 由于創(chuàng)建隊列代碼為
dispatch_queue_create("XXX", NULL)
更哄,所以搜索dispatch_queue_create(
—— 將篩選結(jié)果降至(21 results in 6 files)
- 由于第一個參數(shù)為字符串芋齿,在
c語言
中用const
修飾,所以搜索dispatch_queue_create(const
—— 將篩選結(jié)果降至(2 results in 2 files)
②.1 dispatch_queue_create
常規(guī)中間層封裝 —— 便于代碼迭代不改變上層使用
有時候也需要注意下源碼中函數(shù)中的傳參:
- 此時
label
是上層的逆序全程域名
成翩,主要用在崩潰調(diào)試 -
attr
是NULL/DISPATCH_QUEUE_SERIAL觅捆、DISPATCH_QUEUE_CONCURRENT
,用于區(qū)分隊列是異步還是同步的
#define DISPATCH_QUEUE_SERIAL NULL
串行隊列的宏定義其實是個NULL
②.2 _dispatch_lane_create_with_target
-
1.通過
_dispatch_queue_attr_to_info
方法傳入dqa
(即隊列類型麻敌,串行栅炒、并發(fā)
等)創(chuàng)建dispatch_queue_attr_info_t
類型的對象dqai
,用于存儲隊列的相關(guān)屬性信息
-
dispatch_queue_attr_info_t
與isa
一樣术羔,是個位域結(jié)構(gòu),用于存儲隊列的相關(guān)屬性信息
-
- 2.設(shè)置隊列相關(guān)聯(lián)的屬性赢赊,例如服務(wù)質(zhì)量qos等
- 3.通過
DISPATCH_VTABLE
拼接隊列名稱,即vtable
级历,其中DISPATCH_VTABLE
是宏定義释移,如下所示,所以隊列的類型是通過OS_dispatch_
+隊列類型queue_concurrent
拼接而成的- 串行隊列類型:
OS_dispatch_queue_serial
寥殖,驗證如下
- 串行隊列類型:
* 并發(fā)隊列類型:`OS_dispatch_queue_concurrent`玩讳,驗證如下![](https://upload-images.jianshu.io/upload_images/2340353-ac444e89cd7608be.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- 4.通過
alloc+init
初始化隊列,即dq
扛禽,其中在_dispatch_queue_init
傳參中根據(jù)dqai.dqai_concurrent
的布爾值锋边,就能判斷隊列是串行
還是并發(fā)
,而vtable
表示隊列的類型编曼,說明隊列也是對象- 進入
_dispatch_object_alloc -> _os_object_alloc_realized
方法中設(shè)置了isa
的指向,從這里可以驗證隊列也是對象
的說法
- 進入
* 進入`_dispatch_queue_init`方法,隊列類型是`dispatch_queue_t`,并設(shè)置隊列的相關(guān)屬性![](https://upload-images.jianshu.io/upload_images/2340353-bc9fbe87c7aea2e9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- 5.通過
_dispatch_trace_queue_create
對創(chuàng)建的隊列進行處理剩辟,其中_dispatch_trace_queue_create
是_dispatch_introspection_queue_create
封裝的宏定義掐场,最后會返回處理過的_dq
* 進入`_dispatch_introspection_queue_create_hook -> dispatch_introspection_queue_get_info -> _dispatch_introspection_lane_get_info`中可以看出往扔,與我們自定義的類還是有所區(qū)別的,`創(chuàng)建隊列`在底層的實現(xiàn)是`通過模板創(chuàng)建`的![](https://upload-images.jianshu.io/upload_images/2340353-8558202b2f11d069.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
②.3 總結(jié)
- 隊列創(chuàng)建方法
dispatch_queue_create
中的參數(shù)二(即隊列類型)熊户,決定了下層中max & 1
(用于區(qū)分是 串行 還是 并發(fā))萍膛,其中1表示串行
-
queue
也是一個對象,也需要底層通過alloc + init
創(chuàng)建嚷堡,并且在alloc
中也有一個class
蝗罗,這個class
是通過宏定義拼接而成,并且同時會指定isa
的指向 -
創(chuàng)建隊列
在底層的處理是通過模板創(chuàng)建的蝌戒,其類型是dispatch_introspection_queue_s
結(jié)構(gòu)體
dispatch_queue_create
底層分析流程如下圖所示
③ 異步函數(shù)
③.1 dispatch_async
主要分析兩個函數(shù)
-
_dispatch_continuation_init
:任務(wù)包裝函數(shù) -
_dispatch_continuation_async
:并發(fā)處理函數(shù)
③.2 _dispatch_continuation_init 任務(wù)包裝器
主要是包裝任務(wù)串塑,并設(shè)置線程的回程函數(shù),相當(dāng)于初始化
主要有以下幾步
- 通過
_dispatch_Block_copy
拷貝任務(wù) - 通過
_dispatch_Block_invoke
封裝任務(wù)北苟,其中_dispatch_Block_invoke
是個宏定義桩匪,根據(jù)以上分析得知是異步回調(diào)
- 如果是
同步
的,則回調(diào)函數(shù)賦值為_dispatch_call_block_and_release
- 通過
_dispatch_continuation_init_f
方法將回調(diào)函數(shù)賦值友鼻,即f
就是func
傻昙,將其保存在屬性中
③.3 _dispatch_continuation_async 并發(fā)處理
這個函數(shù)中,主要是執(zhí)行block回調(diào)
- 其中的關(guān)鍵代碼是
dx_push(dqu._dq, dc, qos)
彩扔,dx_push
是宏定義妆档,如下所示
- 而其中的
dq_push
需要根據(jù)隊列的類型,執(zhí)行不同的函數(shù)
在此我們通過符號斷點調(diào)試執(zhí)行函數(shù)
- 運行
demo
虫碉,通過符號斷點贾惦,來判斷執(zhí)行的是哪個函數(shù),由于是并發(fā)隊列蔗衡,通過增加_dispatch_lane_concurrent_push
符號斷點纤虽,看看是否會走到這里
- 運行發(fā)現(xiàn),走的確實是
_dispatch_lane_concurrent_push
- 進入
_dispatch_lane_concurrent_push
源碼绞惦,發(fā)現(xiàn)有兩步逼纸,繼續(xù)通過符號斷點_dispatch_continuation_redirect_push
和_dispatch_lane_push
調(diào)試,發(fā)現(xiàn)走的是_dispatch_continuation_redirect_push
- 進入
_dispatch_continuation_redirect_push
源碼济蝉,發(fā)現(xiàn)又走到了dx_push
杰刽,即遞歸了,綜合前面隊列創(chuàng)建時可知王滤,隊列也是一個對象贺嫂,有父類、根類雁乡,所以會遞歸執(zhí)行到根類的方法
- 接下來第喳,通過根類的
_dispatch_root_queue_push
符號斷點,來驗證猜想是否正確踱稍,從運行結(jié)果看出曲饱,完全是正確的
- 進入
_dispatch_root_queue_push -> _dispatch_root_queue_push_inline ->_dispatch_root_queue_poke -> _dispatch_root_queue_poke_slow
源碼悠抹,經(jīng)過符號斷點驗證,確實是走的這里扩淀,查看該方法的源碼實現(xiàn)楔敌,主要有兩步操作- 通過
_dispatch_root_queues_init
方法注冊回調(diào) - 通過
do-while
循環(huán)創(chuàng)建線程,使用pthread_create
方法
- 通過
③.4 _dispatch_root_queues_init
- 進入
_dispatch_root_queues_init
源碼實現(xiàn)驻谆,發(fā)現(xiàn)是一個dispatch_once_f
單例(請查看后續(xù)單例的底層分析們卵凑,這里不作說明),其中傳入的func
是_dispatch_root_queues_init_once
- 進入
_dispatch_root_queues_init_once
的源碼胜臊,其內(nèi)部不同事務(wù)的調(diào)用句柄都是_dispatch_worker_thread2
其block
回調(diào)執(zhí)行的調(diào)用路徑為:_dispatch_root_queues_init_once ->_dispatch_worker_thread2 -> _dispatch_root_queue_drain -> _dispatch_root_queue_drain -> _dispatch_continuation_pop_inline -> _dispatch_continuation_invoke_inline -> _dispatch_client_callout -> dispatch_call_block_and_release
這個路徑可以通過斷點勺卢,bt
打印堆棧信息得出
在這里需要說明一點的是,單例
的block
回調(diào)和異步函數(shù)
的block
回調(diào)是不同的
- 單例中区端,
block
回調(diào)中的func
是_dispatch_Block_invoke(block)
- 而異步函數(shù)中值漫,
block
回調(diào)中的func
是dispatch_call_block_and_release
④ 總結(jié)
綜上所述,異步函數(shù)的底層分析如下
-
準(zhǔn)備工作
: 首先织盼,將異步任務(wù)拷貝并封裝杨何,并設(shè)置回調(diào)函數(shù)func
-
block回調(diào)
:底層通過dx_push
遞歸,會重定向到根隊列沥邻,然后通過pthread_creat
創(chuàng)建線程危虱,最后通過dx_invoke
執(zhí)行block
回調(diào)(注意dx_push
和dx_invoke
是成對的)
異步函數(shù)的底層分析流程如圖所示
④ 同步函數(shù)
④.1 dispatch_sync
其底層的實現(xiàn)是通過柵欄函數(shù)
實現(xiàn)的(柵欄函數(shù)的底層分析見后文)
④.2 _dispatch_sync_f
④.3 _dispatch_sync_f_inline
查看_dispatch_sync_f_inline
源碼,其中width = 1
表示是串行隊列
唐全,其中有兩個重點:
- 柵欄:
_dispatch_barrier_sync_f
(可以通過后文的柵欄函數(shù)底層分析解釋)埃跷,可以得出同步函數(shù)
的底層實現(xiàn)其實是同步柵欄函數(shù)
- 死鎖:
_dispatch_sync_f_slow
,如果存在相互等待的情況邮利,就會造成死鎖
④.4 _dispatch_sync_f_slow 死鎖
進入_dispatch_sync_f_slow
弥雹,當(dāng)前的主隊列
是掛起、阻塞
的
- 往一個隊列中加入任務(wù)延届,會
push
加入主隊列剪勿,進入_dispatch_trace_item_push
- 進入
__DISPATCH_WAIT_FOR_QUEUE__
,判斷dq
是否為正在等待的隊列方庭,然后給出一個狀態(tài)state
厕吉,然后將dq
的狀態(tài)和當(dāng)前任務(wù)依賴的隊列進行匹配
- 進入
_dq_state_drain_locked_by -> _dispatch_lock_is_locked_by
源碼
如果當(dāng)前等待的和正在執(zhí)行的是同一個隊列,即判斷線程ID
是否相等械念,如果相等头朱,則會造成死鎖
同步函數(shù) + 并發(fā)隊列 順序執(zhí)行的原因
在_dispatch_sync_invoke_and_complete -> _dispatch_sync_function_invoke_inline
源碼中,主要有三個步驟:
- 將任務(wù)壓入隊列:
_dispatch_thread_frame_push
- 執(zhí)行任務(wù)的
block
回調(diào):_dispatch_client_callout
- 將任務(wù)出隊:
_dispatch_thread_frame_pop
從實現(xiàn)中可以看出龄减,是先將任務(wù)push
隊列中项钮,然后執(zhí)行block
回調(diào),在將任務(wù)pop
,所以任務(wù)是順序執(zhí)行的
④.5 總結(jié)
同步函數(shù)的底層實現(xiàn)如下:
-
同步函數(shù)
的底層實現(xiàn)實際是同步柵欄函數(shù)
- 同步函數(shù)中如果
當(dāng)前正在執(zhí)行的隊列和等待的是同一個隊列
寄纵,形成相互等待
的局面鳖敷,則會造成死鎖
⑤ dispatch_once 單例
在日常開發(fā)中程拭,我們一般使用GCD
的dispatch_once
來創(chuàng)建單例,如下所示
首先對于單例棍潘,我們需要了解兩點
-
執(zhí)行一次的原因
: 單例的流程只執(zhí)行一次恃鞋,底層是如何控制的,即為什么只能執(zhí)行一次亦歉? -
block調(diào)用時機
: 單例的block
是在什么時候進行調(diào)用的恤浪?
下面帶著以上兩點疑問,我們來針對單例的底層進行分析
⑤.1 dispatch_once
進入dispatch_once
源碼實現(xiàn),底層是通過dispatch_once_f
實現(xiàn)的
- 參數(shù)1:
onceToken
肴楷,它是一個靜態(tài)變量水由,由于不同位置定義的靜態(tài)變量是不同的,所以靜態(tài)變量具有唯一性 - 參數(shù)2:
block
回調(diào)
⑤.2 dispatch_once_f
進入dispatch_once_f
源碼赛蔫,其中的val
是外界傳入的onceToken
靜態(tài)變量砂客,而func
是_dispatch_Block_invoke(block)
,其中單例的底層主要分為以下幾步
- 將
val
呵恢,也就是靜態(tài)變量
轉(zhuǎn)換為dispatch_once_gate_t
類型的變量l
- 通過
os_atomic_load
獲取此時的任務(wù)的標(biāo)識符v
- 如果
v
等于DLOCK_ONCE_DONE
鞠值,表示任務(wù)已經(jīng)執(zhí)行過了,直接return
- 如果 任務(wù)執(zhí)行后渗钉,
加鎖失敗
了彤恶,則走到_dispatch_once_mark_done_if_quiesced
函數(shù),再次進行存儲鳄橘,將標(biāo)識符置為DLOCK_ONCE_DONE
- 反之声离,則通過
_dispatch_once_gate_tryenter
嘗試進入任務(wù),即解鎖瘫怜,然后執(zhí)行_dispatch_once_callout
執(zhí)行block
回調(diào)
- 如果
- 如果此時有任務(wù)正在執(zhí)行术徊,再次進來一個任務(wù)2,則通過
_dispatch_once_wait
函數(shù)讓任務(wù)2進入無限次等待
⑤.3 _dispatch_once_gate_tryenter 解鎖
查看其源碼宝磨,主要是通過底層os_atomic_cmpxchg
方法進行對比弧关,如果比較沒有問題,則進行加鎖唤锉,即任務(wù)的標(biāo)識符置為DLOCK_ONCE_UNLOCKED
⑤.4 _dispatch_once_callout 回調(diào)
進入_dispatch_once_callout
源碼世囊,主要就兩步
-
_dispatch_client_callout
:block
回調(diào)執(zhí)行 -
_dispatch_once_gate_broadcast
:進行廣播
- 進入
_dispatch_client_callout
源碼,主要就是執(zhí)行block
回調(diào)窿祥,其中的f
等于_dispatch_Block_invoke(block)
株憾,即異步回調(diào)
- 進入
_dispatch_once_gate_broadcast -> _dispatch_once_mark_done
源碼,主要就是給dgo->dgo_once
一個值,然后將任務(wù)的標(biāo)識符為DLOCK_ONCE_DONE
嗤瞎,即解鎖
⑤.5 總結(jié)
針對單例的底層實現(xiàn)墙歪,主要說明如下:
單例只執(zhí)行一次的原理
:GCD
單例中,有兩個重要參數(shù)贝奇,onceToken
和block
虹菲,其中onceToken
是靜態(tài)變量,具有唯一性掉瞳,在底層被封裝成了dispatch_once_gate_t
類型的變量l
毕源,l
主要是用來獲取底層原子封裝性的關(guān)聯(lián),即變量v
陕习,通過v
來查詢?nèi)蝿?wù)的狀態(tài)霎褐,如果此時v
等于DLOCK_ONCE_DONE
,說明任務(wù)已經(jīng)處理過一次了该镣,直接return
block調(diào)用時機
:如果此時任務(wù)沒有執(zhí)行過冻璃,則會在底層通過C++
函數(shù)的比較,將任務(wù)進行加鎖
损合,即任務(wù)狀態(tài)置為DLOCK_ONCE_UNLOCK
省艳,目的是為了保證當(dāng)前任務(wù)執(zhí)行的唯一性申屹,防止在其他地方有多次定義.加鎖之后進行block
回調(diào)函數(shù)的執(zhí)行植阴,執(zhí)行完成后磅废,將當(dāng)前任務(wù)解鎖
减宣,將當(dāng)前的任務(wù)狀態(tài)置為DLOCK_ONCE_DONE
矫渔,在下次進來時雹有,就不會在執(zhí)行剥汤,會直接返回多線程影響
:如果在當(dāng)前任務(wù)執(zhí)行期間免姿,有其他任務(wù)進來擦耀,會進入無限次等待棉圈,原因是當(dāng)前任務(wù)已經(jīng)獲取了鎖,進行了加鎖眷蜓,其他任務(wù)是無法獲取鎖的
單例的底層流程分析如下如所示
⑥ 柵欄函數(shù)
GCD
中常用的柵欄函數(shù)分瘾,主要有兩種
-
同步
柵欄函數(shù)dispatch_barrier_sync
(在主線程中執(zhí)行):前面的任務(wù)執(zhí)行完畢才會來到這里,但是同步柵欄函數(shù)會堵塞線程
吁系,影響后面的任務(wù)執(zhí)行 -
異步
柵欄函數(shù)dispatch_barrier_async
:前面的任務(wù)執(zhí)行完畢才會來到這里
柵欄函數(shù)最直接的作用
就是 控制任務(wù)執(zhí)行順序德召,使同步執(zhí)行
柵欄函數(shù)需要注意以下幾點
- 柵欄函數(shù)只能控制
同一并發(fā)隊列
-
同步柵欄
添加進入隊列的時候,當(dāng)前線程會被鎖死
汽纤,直到同步柵欄之前的任務(wù)和同步柵欄任務(wù)本身執(zhí)行完畢時上岗,當(dāng)前線程才會打開然后繼續(xù)執(zhí)行下一句代碼 - 在使用柵欄函數(shù)時,使用
自定義隊列
才有意義- 如果柵欄函數(shù)中使用
全局隊列
,運行會崩潰
蕴坪,原因是系統(tǒng)也在用全局并發(fā)隊列肴掷,使用柵欄同時會攔截系統(tǒng)的敬锐,所以會崩潰 - 如果將自定義并發(fā)隊列改為串行隊列,即serial 呆瞻,串行隊列本身就是有序同步 此時加?xùn)艡谔ǘ幔瑫速M性能
- 如果柵欄函數(shù)中使用
⑥.1 異步柵欄函數(shù)
進入dispatch_barrier_async
源碼實現(xiàn),其底層的實現(xiàn)與dispatch_async
類似痴脾,這里就不再做分析了颤介,有興趣的可以自行探索下
⑥.2 同步柵欄函數(shù)
⑥.2.1 dispatch_barrier_sync
進入dispatch_barrier_sync
源碼,實現(xiàn)如下
⑥.2.2 _dispatch_barrier_sync_f_inline
進入_dispatch_barrier_sync_f -> _dispatch_barrier_sync_f_inline
源碼
主要有分為以下幾部分
- 通過
_dispatch_tid_self
獲取線程ID
- 通過
_dispatch_queue_try_acquire_barrier_sync
判斷線程狀態(tài)
* 進入`_dispatch_queue_try_acquire_barrier_sync_and_suspend`明郭,在這里進行釋放![](https://upload-images.jianshu.io/upload_images/2340353-4ccaf0f24ed3b69a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- 通過
_dispatch_sync_recurse
遞歸查找柵欄函數(shù)的target
- 通過
_dispatch_introspection_sync_begin
對向前信息進行處理
- 通過
_dispatch_lane_barrier_sync_invoke_and_complete
執(zhí)行block
并釋放
⑦ 信號量
信號量的作用一般是用來使任務(wù)同步執(zhí)行
买窟,類似于互斥鎖
,用戶可以根據(jù)需要控制GCD
最大并發(fā)數(shù).前面我們已經(jīng)說了怎么使用了
下面我們來分析其底層原理
⑦.1 dispatch_semaphore_create 創(chuàng)建
該函數(shù)的底層實現(xiàn)如下薯定,主要是初始化信號量
,并設(shè)置GCD的最大并發(fā)數(shù)瞳购,其最大并發(fā)數(shù)必須大于0
⑦.2 dispatch_semaphore_wait 加鎖
該函數(shù)的源碼實現(xiàn)如下话侄,其主要作用是對信號量dsema
通過os_atomic_dec2o
進行了--
操作,其內(nèi)部是執(zhí)行的C++
的atomic_fetch_sub_explicit
方法
- 如果
value >= 0
学赛,表示操作無效年堆,即執(zhí)行成功 - 如果
value = LONG_MIN
,系統(tǒng)會拋出一個crash
- 如果
value < 0
盏浇,則進入長等待
其中os_atomic_dec2o
的宏定義轉(zhuǎn)換如下
os_atomic_inc2o(p, f, m)
os_atomic_sub2o(p, f, 1, m)
_os_atomic_c11_op((p), (v), m, sub, -)
_os_atomic_c11_op((p), (v), m, add, +)
({ _os_atomic_basetypeof(p) _v = (v), _r = \
atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), _v, \
memory_order_##m); (__typeof__(_r))(_r op _v); })
將具體的值代入為
os_atomic_dec2o(dsema, dsema_value, acquire);
os_atomic_sub2o(dsema, dsema_value, 1, m)
os_atomic_sub(dsema->dsema_value, 1, m)
_os_atomic_c11_op(dsema->dsema_value, 1, m, sub, -)
_r = atomic_fetch_sub_explicit(dsema->dsema_value, 1),
等價于 dsema->dsema_value - 1
進入_dispatch_semaphore_wait_slow
的源碼實現(xiàn)变丧,當(dāng)value < 0
時,根據(jù)等待事件timeout
做出不同操作
⑦.3 dispatch_semaphore_signal 解鎖
該函數(shù)的源碼實現(xiàn)如下绢掰,其核心也是通過os_atomic_inc2o
函數(shù)對value
進行了++
操作痒蓬,os_atomic_inc2o
內(nèi)部是通過C++
的atomic_fetch_add_explicit
- 如果
value > 0
,表示操作無效滴劲,即執(zhí)行成功 - 如果
value < 0
攻晒,則進入長等待
其中os_atomic_dec2o的宏定義轉(zhuǎn)換如下
os_atomic_inc2o(p, f, m)
os_atomic_add2o(p, f, 1, m)
os_atomic_add(&(p)->f, (v), m)
_os_atomic_c11_op((p), (v), m, add, +)
({ _os_atomic_basetypeof(p) _v = (v), _r = \
atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), _v, \
memory_order_##m); (__typeof__(_r))(_r op _v); })
將具體的值代入為
os_atomic_inc2o(dsema, dsema_value, release);
os_atomic_add2o(dsema, dsema_value, 1, m)
os_atomic_add(&(dsema)->dsema_value, (1), m)
_os_atomic_c11_op((dsema->dsema_value), (1), m, add, +)
_r = atomic_fetch_add_explicit(dsema->dsema_value, 1),
等價于 dsema->dsema_value + 1
⑦.4 總結(jié)
-
dispatch_semaphore_create
主要就是初始化限號量 -
dispatch_semaphore_wait
是對信號量的value
進行--
,即加鎖操作 -
dispatch_semaphore_signal
是對信號量的value
進行++
班挖,即解鎖操作
⑧ 調(diào)度組的原理
調(diào)度組的最直接作用是控制任務(wù)執(zhí)行順序
,常見方式如下
⑧.1 dispatch_group_create
- 進入
dispatch_group_create
源碼
主要是創(chuàng)建group
萧芙,并設(shè)置屬性给梅,此時的group
的value為0
- 進入
_dispatch_group_create_with_count
源碼,其中是對group
對象屬性賦值双揪,并返回group
對象动羽,其中的n等于0
⑧.2 dispatch_group_enter 進組
進入dispatch_group_enter
源碼,通過os_atomic_sub_orig2o
對dg->dg.bits
作 --
操作盟榴,對數(shù)值進行處理
⑧.3 dispatch_group_leave 出組
進入dispatch_group_leave
源碼,可知
- -1 到 0曹质,即
++
操作 - 根據(jù)狀態(tài),
do-while
循環(huán),喚醒執(zhí)行block
任務(wù) - 如果
0 + 1 = 1
羽德,enter-leave
不平衡几莽,即leave
多次調(diào)用,會crash
- 進入
_dispatch_group_wake
源碼宅静,do-while
循環(huán)進行異步命中章蚣,調(diào)用_dispatch_continuation_async
執(zhí)行
- 進入
_dispatch_continuation_async
源碼
這步與異步函數(shù)的block
回調(diào)執(zhí)行是一致的,這里不再作說明
⑧.4 dispatch_group_notify 通知
進入dispatch_group_notify
源碼姨夹,如果old_state
等于0
纤垂,就可以進行釋放了
除了leave
可以通過_dispatch_group_wake
喚醒,其中dispatch_group_notify
也是可以喚醒的
- 其中
os_mpsc_push_update_tail
是宏定義磷账,用于獲取dg
的狀態(tài)碼
⑧.5 dispatch_group_async
進入dispatch_group_async
源碼峭沦,主要是包裝任務(wù)
和異步處理
任務(wù)
- 進入
_dispatch_continuation_group_async
源碼,主要是封裝了dispatch_group_enter
進組操作
-
進入
_dispatch_continuation_async
源碼逃糟,執(zhí)行常規(guī)的異步函數(shù)底層操作.既然有了enter
吼鱼,肯定有leave
,我們猜測block執(zhí)行之后隱性的執(zhí)行l(wèi)eave
绰咽,通過斷點調(diào)試菇肃,打印堆棧信息 -
搜索
_dispatch_client_callout
的調(diào)用,在_dispatch_continuation_with_group_invoke
中
所以取募,完美的印證dispatch_group_async
底層封裝的是enter-leave
⑧.6 總結(jié)
-
enter-leave
只要成對就可以琐谤,不管遠近 -
dispatch_group_enter
在底層是通過C++
函數(shù),對group
的value
進行--
操作(即0 -> -1
) -
dispatch_group_leave
在底層是通過C++
函數(shù)玩敏,對group
的value
進行++
操作(即-1 -> 0
) -
dispatch_group_notify
在底層主要是判斷group
的state
是否等于0
斗忌,當(dāng)等于0
時,就通知 -
block
任務(wù)的喚醒聊品,可以通過dispatch_group_leave
飞蹂,也可以通過dispatch_group_notify
-
dispatch_group_async
等同于enter - leave
,其底層的實現(xiàn)就是enter-leave
六陈哑、相關(guān)試題解析
① 異步函數(shù)+并行隊列
下面代碼的輸出順序是什么?
異步函數(shù)并不會阻塞主隊列伸眶,會開辟新線程執(zhí)行異步任務(wù)
分析思路如下圖所示惊窖,紅線表示任務(wù)的執(zhí)行順序-
主線程
的任務(wù)隊列為:任務(wù)1、異步block1厘贼、任務(wù)5
界酒,其中異步block1
會比較耗費性能,任務(wù)1
和任務(wù)5
的任務(wù)復(fù)雜度是相同的嘴秸,所以任務(wù)1和任務(wù)5優(yōu)先于異步block1執(zhí)行
- 在
異步block1
中毁欣,任務(wù)隊列為:任務(wù)2庇谆、異步block2、任務(wù)4
凭疮,其中block2
相對比較耗費性能饭耳,任務(wù)2
和任務(wù)4
是復(fù)雜度一樣,所以任務(wù)2和任務(wù)4優(yōu)先于block2執(zhí)行
- 最后執(zhí)行
block2
中的任務(wù)3
- 在極端情況下执解,可能出現(xiàn)
任務(wù)2
先于任務(wù)1
和任務(wù)5
執(zhí)行寞肖,原因是出現(xiàn)了當(dāng)前主線程卡頓
或者延遲
的情況
擴展一
將并行隊列
改成 串行隊列
,對結(jié)果沒有任何影響衰腌,順序仍然是1 5 2 4 3
擴展二
在任務(wù)5之前新蟆,休眠2s,即sleep(2)
右蕊,執(zhí)行的順序為:1 2 4 3 5
,原因是因為I/O的打印琼稻,相比于休眠2s,復(fù)雜度更簡單尤泽,所以異步block1
會先于任務(wù)5
執(zhí)行.當(dāng)然如果主隊列堵塞欣簇,會出現(xiàn)其他的執(zhí)行順序
② 異步函數(shù)嵌套同步函數(shù) + 并發(fā)隊列
下面代碼的輸出順序是什么?
分析如下:
-
任務(wù)1
和任務(wù)5
的分析同前面一致坯约,執(zhí)行順序為任務(wù)1 任務(wù)5 異步block
- 在
異步block
中,首先執(zhí)行任務(wù)2
莫鸭,然后走到同步block
闹丐,由于同步函數(shù)會阻塞主線程,所以任務(wù)4
需要等待任務(wù)3
執(zhí)行完成后被因,才能執(zhí)行卿拴,所以異步block
中的執(zhí)行順序是:任務(wù)2 任務(wù)3 任務(wù)4
③ 異步函數(shù)嵌套同步函數(shù) + 串行隊列(即同步隊列)
下面代碼的執(zhí)行順序是什么?會出現(xiàn)什么情況梨与?為什么堕花?
- 首先執(zhí)行
任務(wù)1
缘挽,接下來是異步block
,并不會阻塞主線程呻粹,相比任務(wù)5而言壕曼,復(fù)雜度更高,所以優(yōu)先執(zhí)行任務(wù)5
等浊,在執(zhí)行異步block
- 在
異步block
中腮郊,先執(zhí)行任務(wù)2
,接下來是同步block
筹燕,同步函數(shù)會阻塞線程
轧飞,所以執(zhí)行任務(wù)4需要等待任務(wù)3執(zhí)行完成
衅鹿,而任務(wù)3
的執(zhí)行,需要等待異步block執(zhí)行完成
过咬,相當(dāng)于任務(wù)3等待任務(wù)4
完成 - 所以就造成了
任務(wù)4等待任務(wù)3
大渤,任務(wù)3等待任務(wù)4
,即互相等待的局面援奢,就會造成死鎖
兼犯,這里有個重點是關(guān)鍵的堆棧slow
擴展一
去掉任務(wù)4
,執(zhí)行順序是什么集漾?
還是會死鎖
切黔,因為任務(wù)3等待的是異步block執(zhí)行完畢
,而異步block等待任務(wù)3
.
④ 異步函數(shù) + 同步函數(shù) + 并發(fā)隊列
下面代碼的執(zhí)行順序是什么具篇?(答案是 AC)
A: 1230789
B: 1237890
C: 3120798
D: 2137890
分析
-
任務(wù)1
和任務(wù)2
由于是異步函數(shù)+并發(fā)隊列
纬霞,會開啟線程,所以沒有固定順序 -
任務(wù)7
驱显、任務(wù)8
诗芜、任務(wù)9
同理,會開啟線程埃疫,所以沒有固定順序 -
任務(wù)3
是同步函數(shù)+并發(fā)隊列
伏恐,同步函數(shù)會阻塞主線程,但是也只會阻塞0
栓霜,所以翠桦,可以確定的是0一定在3之后
,在789之前
以下是不同的執(zhí)行順序的打印
⑤ 下面代碼中胳蛮,隊列的類型有幾種销凑?
隊列總共有兩種: 并發(fā)隊列
和 串行隊列
- 串行隊列:
serial
、mainQueue
- 并發(fā)隊列:
conque
仅炊、globalQueue
寫在后面
和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.