目錄
前言
基本概念及原理
線程、進程與隊列
線程的定義:
進程的定義:
線程與進程之間的聯系與區別:
線程和runloop的關系
影響任務執行速度的因素
多線程
多線程生命周期
線程池的原理
iOS中多線程的實現方式
線程安全問題
互斥鎖
自旋鎖
對比GCD和NSOperation
NSThread
GCD
函數
隊列
函數與隊列的不同組合
串行隊列 + 同步派發
?編輯串行隊列 + 異步派發
?編輯
并發隊列 + 同步派發
?編輯
并發隊列 + 異步派發
主隊列 + 同步函數
主隊列 + 異步派發
dispatch_after
dispatch_once
dispatch_apply
dispatch_group_t
dispatch_group_async + dispatch_group_notify
dispatch_group_enter + dispatch_group_leave + dispatch_group_notify
dispatch_barrier_sync & dispatch_barrier_async
dispatch_semaphore_t
dispatch_source
NSOperation
NSInvocationOperation
NSBlockOperation
NSOperationQueue
設置優先級
設置并發數
設置依賴
前言
多線程在iOS的開發中起到了非常重要的作用,筆者在之前已經有學習過關于GCD的知識了,但是當時學得迷迷糊糊,正好借學習多線程底層原理的機會,來在學習多線程的同時對之前的知識做一個復習
基本概念及原理
線程、進程與隊列
線程的定義:
-
線程是進程的基本執行單元,一個進程的所有任務都在線程中執行
-
進程想要執行任務,必須得有線程,
進程至少要有一條線程
-
程序啟動會默認開啟一條線程
,這條線程被成為主線程
或UI線程
進程的定義:
-
進程
是指在系統中正在運行的一個應用程序,如微信、支付寶app都是一個進程 -
每個
進程
之間是獨立的,每個進程均運行在其專用的且受保護的內存空間內
通俗地說,可以理解為:進程是線程的容器,而線程用來執行任務。在iOS
中是單進程開發,一個進程就是一個app
,進程之間是相互獨立的,如支付寶、微信、qq等,這些都是屬于不同的進程。
線程與進程之間的聯系與區別:
-
地址空間:同一
進程
的線程
共享本進程的地址空間,而進程之間則是獨立的地址空間 -
資源擁有:同一
進程
內的線程
共享本進程的資源如內存、I/O、cpu等,但是進程
之間的資源是獨立的 -
一個
進程
崩潰后,在保護模式下不會對其他進程
產生影響,但是一個線程
崩潰整個進程
都死掉,所以多進程
要比多線程
健壯 -
進程
切換時,消耗的資源大、效率高.所以設計到頻繁的切換時,使用線程
要好于進程
。同樣如果要求同時進行并且又要共享某些變量的并發操作,只能用線程
而不能用進程
-
線程
是處理器調度的基本單位,但進程
不是 -
線程沒有地址空間,線程包含在進程地址空間中
線程和runloop的關系
-
runloop與線程是一一對應的
—— 一個runloop
對應一個核心的線程
,為什么說是核心的,是因為runloop
是可以嵌套的,但是核心的只能有一個,他們的關系保存在一個全局的字典里 -
runloop是來管理線程的
—— 當線程的runloop
被開啟后,線程會在執行完任務后進入休眠狀態,有了任務就會被喚醒去執行任務 -
runloop
在第一次獲取時被創建,在線程結束時被銷毀-
對于主線程來說,
runloop
在程序一啟動就默認創建好了 -
對于子線程來說,
runloop
是懶加載的 —— 只有當我們使用的時候才會創建,所以在子線程用定時器要注意:確保子線程的runloop被創建,不然定時器不會回調
-
影響任務執行速度的因素
以下因素都會對任務的執行速度造成影響:
-
cpu
的調度 -
線程的執行速率
-
隊列情況
-
任務執行的復雜度
-
任務的優先級
多線程
多線程生命周期
多線程的生命周期主要分為5部分:新建 - 就緒 - 運行 - 阻塞 - 死亡
-
新建:實例化線程對象
-
就緒:線程對象調用
start
方法,將線程對象加入可調度線程池
,等待CPU的調用
,即調用start
方法,并不會立即執行,進入就緒狀態
,需要等待一段時間,經CPU
調度后才執行,也就是從就緒狀態進入運行狀態
-
運行:
CPU
負責調度可調度線程池中線程的執行。在線程執行完成之前,其狀態可能會在就緒和運行之間來回切換.就緒和運行之間的狀態變化由CPU負責,程序員不能干預 -
阻塞:當滿足某個預定條件時,可以
使用休眠或鎖
,阻塞線程執行。sleepForTimeInterval
(休眠指定時長),sleepUntilDate
(休眠到指定日期),@synchronized(self)
:(互斥鎖) -
死亡:正常死亡,即線程執行完畢。非正常死亡,即當滿足某個條件后,在線程內部(或者主線程中)終止執行(調用exit方法等退出)
處于運行中的線程
擁有一段可以執行的時間(稱為時間片):
-
如果
時間片用盡
,線程就會進入就緒狀態隊列
-
如果
時間片沒有用盡
,且需要開始等待某事件
,就會進入阻塞狀態隊列
-
等待事件發生后,線程又會重新進入
就緒狀態隊列
-
每當一個
線程離開運行
,即執行完畢或者強制退出后,會重新從就緒狀態隊列
中選擇一個線程繼續執行
關于線程的exit和cancel方法:
-
exit
:一旦強行終止線程,后續的所有代碼都不會執行 -
cancel
:取消當前線程,但是不能取消正在執行的線程
線程池的原理
可以看到主要分為以下四步:
-
判斷核心線程池是否都正在執行任務:
-
返回NO,創建新的工作線程去執行
-
返回YES,進行第二步
-
-
判斷線程池工作隊列是否已經飽滿:
-
返回NO,將任務存儲到工作隊列,等待CPU調度
-
返回YES,進入第三步
-
-
判斷線程池中的線程是否都處于執行狀態
-
返回NO,安排可調度線程池中空閑的線程去執行任務
-
返回YES,進入第四步
-
-
交給飽和策略去執行,主要有以下四種:
-
AbortPolicy
:直接拋出RejectedExecutionExeception
異常來阻止系統正常運行 -
CallerRunsPolicy
:將任務回退到調用者 -
DisOldestPolicy
:丟掉等待最久的任務 -
DisCardPolicy
:直接丟棄任務
-
iOS中多線程的實現方式
iOS中多線程的實現方式主要有四種:pthread、NSThread、GCD、NSOperation
線程安全問題
當多個線程同時訪問一塊內存,容易引發數據錯亂和數據安全問題,有以下兩種解決方案:
-
互斥鎖(即同步鎖):
@synchronized
-
自旋鎖
互斥鎖
-
保證鎖內的代碼,同一時間,只有一條線程能夠執行!
-
互斥鎖的鎖定范圍,應該盡量小,鎖定范圍越大,效率越差!
-
加了互斥鎖的代碼,當新線程訪問時,如果發現其他線程正在執行鎖定的代碼,新線程就會進入休眠
-
能夠加鎖的是任意 NSObject 對象,但必須是 NSObject 對象
-
鎖對象必須保證所有線程都能訪問
-
單點加鎖時推薦使用 self
自旋鎖
-
自旋鎖與互斥鎖類似,但它不是通過休眠使線程阻塞,而是在獲取鎖之前一直處于
忙等
(即原地打轉,稱為自旋)阻塞狀態 -
使用場景:鎖持有的時間短,且線程不希望在重新調度上花太多成本時,就需要使用自旋鎖,屬性修飾符
atomic
,本身就有一把自旋鎖
-
加入了自旋鎖,當新線程訪問代碼時,如果發現有其他線程正在鎖定代碼,新線程會用
死循環
的方法,一直等待鎖定的代碼執行完成,即不停的嘗試執行代碼,比較消耗性能
、 -
atomic
本身就有一把鎖(自旋鎖
)
iOS開發的建議:
所有屬性都聲明為
nonatomic
盡量避免多線程搶奪同一塊資源 盡量將加鎖、資源搶奪的業務邏輯交給服務器端處理,減小移動客戶端的壓力
對比GCD和NSOperation
GCD
和NSOperation
的關系如下:
-
GCD
是面向底層的C
語言的API
-
NSOperation
是用GCD
封裝構建的,是GCD
的高級抽象
GCD和NSOperation的對比如下:
-
GCD
執行效率更高,而且由于隊列中執行的是由block
構成的任務,這是一個輕量級的數據結構 —— 寫起來更加方便 -
GCD
只支持FIFO
的隊列,而NSOpration
可以設置最大并發數、設置優先級、添加依賴關系等調整執行順序 -
NSOpration
甚至可以跨隊列設置依賴關系,但是GCD
只能通過設置串行隊列,或者在隊列內添加barrier
任務才能控制執行順序,較為復雜 -
NSOperation
支持KVO
(面向對象)可以檢測operation
是否正在執行、是否結束、是否取消(如果是自定義的NSOperation 子類,需要手動觸發KVO通知)
NSThread
NSthread
是蘋果官方提供面向對象的線程操作技術,是對thread
的上層封裝,比較偏向于底層。
通過NSThread創建線程的方式主要有以下三種方式:
-
通過
init
初始化方式創建 -
通過
detachNewThreadSelector
構造器方式創建 -
通過
performSelector...
方法創建,主要是用于獲取主線程
,以及后臺線程
NSThread常用的類方法有以下:
-
currentThread
:獲取當前線程 -
sleep...
:阻塞線程 -
exit
:退出線程 -
mainThread
:獲取主線程
GCD
GCD就是Grand Central Dispatch
,它是純 C
語言。關于GCD,筆者之前已經有博客詳細介紹過它的概念和接口以及用法了。這里對于GCD的簡單概念就不重復贅述了,詳情可以點擊筆者這篇博客——OC高級編程之GCD。這里就直接對GCD的函數與隊列來進行一個再梳理和復習吧
函數
在GCD
中執行任務的方式有兩種,同步執行
和異步執行
,分別對應同步函數dispatch_sync
和 異步函數dispatch_async
。
-
同步執行,對應同步函數
dispatch_sync
-
必須等待當前語句執行完畢,才會執行下一條語句
-
不會開啟線程
,即不具備開啟新線程的能力 -
在當前線程中執行
block
任務
-
-
異步執行,對應異步函數
dispatch_async
-
不用等待當前語句執行完畢,就可以執行下一條語句
-
會開啟線程
執行block
任務,即具備開啟新線程的能力(但并不一定開啟新線程,這個與任務所指定的隊列類型有關) -
異步是多線程的代名詞
-
綜上所述,兩種執行方式的主要區別
有兩點:
-
是否等待
隊列的任務執行完畢 -
是否具備開啟新線程
的能力
隊列
多線程中所說的隊列
(Dispatch Queue
)是指執行任務的等待隊列
,即用來存放任務的隊列.隊列是一種特殊的線性表
,遵循先進先出(FIFO)
原則,即新任務總是被插入到隊尾,而任務的讀取從隊首開始讀取.每讀取一個任務,則動隊列中釋放一個任務。而隊列又分為串行隊列和并發隊列
串行隊列:每次只有一個任務被執行
,等待上一個任務執行完畢再執行下一個,即只開啟一個線程
并發隊列:一次可以并發執行多個任務
,即開啟多個線程
,并同時執行任務
函數與隊列的不同組合
串行隊列 + 同步派發
任務一個接一個地在當前線程執行,不會開辟新線程
串行隊列 + 異步派發
任務一個接一個地執行,但是會開辟新線程
并發隊列 + 同步派發
任務一個接一個地執行,不開辟線程
并發隊列 + 異步派發
任務亂序進行并且會開辟新線程
主隊列 + 同步函數
任務互相等待,造成死鎖
為什么這樣會造成死鎖,這里分析一下原因:
主隊列在執行任務執行到同步block時,會將block的任務加入到主隊列,但由于主隊列是串行隊列,因此block的任務要等主線程執行完block才可以執行(因為當前主線程中任務還沒有執行完,任務應該是進行到執行block了),而執行block其實就是執行block里的任務(即NSLog),主線程等著block里這個任務執行完才執行完,這樣就使得任務之間互相等待,從而造成了死鎖崩潰
死鎖:
-
主線程
因為同步函數
的原因等著先執行任務 -
主隊列
等著主線程的任務執行完畢再執行自己的任務 -
主隊列和主線程
相互等待會造成死鎖
主隊列 + 異步派發
主隊列是一個特殊的串行隊列,它雖然是串行隊列,但是其異步派發不會開辟新線程,而是將任務安排到主線程的下一個運行循環(Run Loop)周期執行
dispatch_after
dispatch_after表示在隊列中的block延遲執行,確切地說是延遲將block加入到隊列
dispatch_once
dispatch_once可以保證在app運行期間,block中的代碼只執行一次,可以用來創建單例
dispatch_apply
dispatch_apply將指定的block追加到指定的隊列中重復執行,并等到全部的處理執行結束(相當于線程安全的for循環)
應用場景:在拉取網絡數據后提前計算出各個控件的大小,防止繪制時計算,提高表單滑動流暢性
dispatch_group_t
dispatch_group_t:調度組將任務分組執行,能監聽任務組完成,并設置等待時間
應用場景:多個接口請求之后刷新頁面
dispatch_group_async + dispatch_group_notify
dispatch_group_notify
在dispatch_group_async
執行結束之后會受收到通知
dispatch_group_enter + dispatch_group_leave + dispatch_group_notify
dispatch_group_enter
和dispatch_group_leave
成對出現,使進出組的邏輯更加清晰
在此基礎上還可以使用 dispatch_group_wait
這里dispatch_group_wait這個函數第一個參數表示要等待的調度組,第二個參數表示要等多久(如果設置為DISPATCH_TIME_NOW表示不等待直接判定是否執行完畢,如果設置為DISPATCH_TIME_FOREVER表示阻塞當前調度組直到調度組執行完畢)
這個函數的返回值為long類型,如果返回值為0,表示在指定時間內調度組完成了任務;如果不為0,表示在指定時間內調度組沒有按時完成任務
dispatch_barrier_sync & dispatch_barrier_async
柵欄函數,主要使用在并發隊列,串行隊列使用柵欄函數沒什么意義。
柵欄函數即:等柵欄前追加到隊列中的任務執行完畢后,再將柵欄后的任務追加到隊列中。 簡而言之,就是先執行柵欄前任務,再執行柵欄任務,最后執行柵欄后任務
可以看到如果沒有柵欄,按照主線程的派發順序,任務2延遲1s,任務1延遲2s,應該是先完成任務2再完成任務1的,但是因為這里有柵欄函數,所以這里任務執行的順序變為:先執行柵欄前的任務1,再執行柵欄任務,然后執行柵欄后的任務2
-
dispatch_barrier_sync與dispatch_barrier_async的作用相同,區別在于是否阻塞線程。
注意??:
1.盡量使用自定義的并發隊列
:
-
使用
全局隊列
起不到柵欄函數
的作用 -
使用
全局隊列
時由于對全局隊列造成堵塞,可能致使系統其他調用全局隊列的地方也堵塞從而導致崩潰(并不是只有你在使用這個隊列)
2.柵欄函數只能控制同一并發隊列
:打個比方,平時在使用AFNetworking
做網絡請求時為什么不能用柵欄函數起到同步鎖堵塞的效果,因為AFNetworking
內部有自己的隊列(也就是說柵欄函數不能跨隊列作用)
dispatch_semaphore_t
dispatch_semaphore_t
表示信號量,可以用來控制GCD最大并發數:
-
dispatch_semaphore_create()
:創建信號量 -
dispatch_semaphore_wait()
:等待信號量,信號量減1
。當信號量< 0
時會阻塞當前線程,根據傳入的等待時間決定接下來的操作——如果永久等待將等到信號(signal)
才執行下去 -
dispatch_semaphore_signal()
:釋放信號量,信號量加1
。當信號量>= 0
會執行wait
之后的代碼
比如用信號量來代替柵欄函數使這段代碼按序輸出:
使用信號量的API來改寫的話就是這樣的:
如果當創建信號量時傳入值為1又會怎么樣呢?
-
i=0
時有可能先打印,也可能會先發出wait
信號量-1,但是wait
之后信號量為0不會阻塞線程,所以進入i=1
-
i=1
時有可能先打印,也可能會先發出wait
信號量-1,但是wait
之后信號量為-1阻塞線程,等待signal
再執行下去
結論:
-
創建信號量時傳入值為1時,可以通過兩次才堵塞
-
傳入值為2時,可以通過三次才堵塞
dispatch_source
dispatch_source
是一種基本的數據類型,可以用來監聽一些底層的系統事件
-
Timer Dispatch Source
:定時器事件源,用來生成周期性的通知或回調 -
Signal Dispatch Source
:監聽信號事件源,當有UNIX信號發生時會通知 -
Descriptor Dispatch Source
:監聽文件或socket
事件源,當文件或socket
數據發生變化時會通知 -
Process Dispatch Source
:監聽進程事件源,與進程相關的事件通知 -
Mach port Dispatch Source
:監聽Mach
端口事件源 -
Custom Dispatch Source
:監聽自定義事件源
主要使用的API:
-
dispatch_source_create
: 創建事件源 -
dispatch_source_set_event_handler
: 設置數據源回調 -
dispatch_source_merge_data
: 設置事件源數據 -
dispatch_source_get_data
: 獲取事件源數據 -
dispatch_resume
: 繼續 -
dispatch_suspend
: 掛起 -
dispatch_cancle
: 取消
比如通過dispatch_source
來實現定時器,在開發中經常使用NSTimer來實現定時邏輯,但是NSTimier是依賴Runloop的,而Runloop可以運行在不同的模式下,如果NSTimer添加在一一種模式下,而Runloop運行在其他模式下,定時器就掛起了;又如果Runloop在阻塞狀態,那么NSTimer的觸發時間就會推遲到下一個Runloop周。因此NSTimer在計時上會有誤差,而GCD計時器不依賴Runloop,計時精度高很多
需要注意??:
-
GCDTimer
需要強持有
,否則出了作用域立即釋放,也就沒有了事件回調 -
GCDTimer
默認是掛起狀態,需要手動激活 -
GCDTimer
沒有repeat
,需要封裝來增加標志位控制 -
GCDTimer
如果存在循環引用,使用weak+strong
或者提前調用dispatch_source_cancel
取消timer
-
dispatch_resume
和dispatch_suspend
調用次數需要平衡 -
source
在掛起狀態
下,如果直接設置source = nil
或者重新創建source
都會造成crash
。正確的方式是在激活狀態下調用dispatch_source_cancel(source)
釋放當前的source
NSOperation
NSOperation
是個抽象類,依賴于子類NSInvocationOperation
、NSBlockOperation
去實現
NSInvocationOperation
也可以直接處理事務,不添加隱性隊列
NSBlockOperation
NSInvocationOperation
和NSBlockOperation
兩者的區別在于:
-
前者類似
target
形式 -
后者類似
block
形式——函數式編程,業務邏輯代碼可讀性更高
NSOperationQueue
是異步執行的,所以任務一
、任務二
的完成順序不確定
通過addExecutionBlock
這個方法可以讓NSBlockOperation
實現多線程
NSBlockOperation創建時block中的任務是在主線程執行,而運用addExecutionBlock加入的任務是在子線程執行的(準確地來說,創建時block中的任務在start調用發生的線程執行)(當Operation沒有添加到隊列,而是通過start調用時)
NSOperationQueue
NSOperationQueue
有兩種隊列:主隊列、其他隊列
-
主隊列:主隊列上的任務是在主線程執行的
-
其他隊列(非主隊列):加入到
非主隊列
中的任務默認就是并發,開啟多線程
通過類方法mainQueue可以得到主隊列
設置優先級
NSOperation
設置優先級只會讓CPU
有更高的幾率調用,不是說設置高就一定全部先完成
通過以下是否讓高優先級任務休眠的任務執行順序即可看出這一點:
不使用sleep:
使用sleep:
設置并發數
在NSOperation中不需使用信號量,直接設置maxConcurrentOperationCount就可以控制并發數,來控制單次出隊列去執行的任務數
設置依賴
在NSOperation
中添加依賴能很好的控制任務執行的先后順序