我們首先來看1張圖:
我覺得這張動圖很好的詮釋了《把1個線程用到死的》核心價值觀。
很多程序都有1個主線程。對iOS/MacOS開發來講,這個線程就是UI線程,在這個線程上會做1些用戶交互/渲染相干的事情。把過量的任務放在主線程,會致使主線程卡頓,進而用戶看到的就是App響應慢,列表轉動的時候掉幀。
把任務分散到多個線程履行有很多種技術,在iOS/MacOS App開發中,最簡單直觀的就是GCD(又叫Dispatch)了。Swift 3把冗雜的GCD API進行了精簡和優化,所以很多時候,我們都可使用GCD來進行多線程開發。
本文使用到的playground可以在我的github上下載到。如果你不熟習Playground的基本操作,歡迎瀏覽的上1篇文章
本文很長,講授范圍從基礎的概念,到async/sync,QoS,Sources,Group,Semaphore,Barrier,再到最后的同步和死鎖,本文的Playground可以在這里下載。
Dispatch comprises language features, runtime libraries, and system enhancements that provide systemic, comprehensive improvements to the support for concurrent code execution on multicore hardware in macOS, iOS, watchOS, and tvOS.
大致的意思是
Dispatch結合語言特性,運行時,和系統的特點,提供了系統的,全面的高層次API來提升多核多線程編程的能力。
Dispatch會自動的根據CPU的使用情況,創建線程來履行任務,并且自動的運行到多核上,提高程序的運行效力。對開發者來講,在GCD層面是沒有線程的概念的,只有隊列(queue)。任務都是以block的方式提交到對列上,然后GCD會自動的創建線程池去履行這些任務。在
對Swift 3來講,Dispatch是1個module.你可以通過import
進行導入
import Dispatch
這里,我們新建1個playgorund來運行本文的釋例代碼,并且命名為Dispatch.playground
關于Swift3.0 中GCD 的改變,參見
DispatchQueue是1個類似線程的概念,這里稱作對列隊列是1個FIFO數據結構,意味著先提交到隊列的任務會先開始履行)。DispatchQueue
背后是1個由系統管理的線程池。
最簡單的,可以依照以下方式初始化1個隊列
//這里的名字能夠方便開發者進行Debug
let queue = DispatchQueue(label: "com.Leo.demoQueue")
這樣初始化的隊列是1個默許配置的隊列,也能夠顯式的指明對列的其他屬性
let label = "com.leo.demoQueue"
let qos = DispatchQoS.default
let attributes = DispatchQueue.Attributes.concurrent
let autoreleaseFrequency = DispatchQueue.AutoreleaseFrequency.never
let queue = DispatchQueue(label: label, qos: qos, attributes: attributes, autoreleaseFrequency: autoreleaseFrequency, target: nil)
這里,我們來1個參數分析他們的作用
label
隊列的標識符,方便調試qos
隊列的quality of service。用來指明隊列的“重要性”,后文會詳細講到。attributes
隊列的屬性。類型是DispatchQueue.Attributes
,是1個結構體,遵守了協議OptionSet。意味著你可以這樣傳入第1個參數[.option1,.option2]autoreleaseFrequency
。顧名思義,自動釋放頻率。有些隊列是會在履行完任務后自動釋放的,有些比如Timer等是不會自動釋放的,是需要手動釋放。從創建者來說,隊列可以分為兩種,其中系統創建的隊列又分為兩種
主隊列/全局隊列可以這樣獲得
let mainQueue = DispatchQueue.main
let globalQueue = DispatchQueue.global()
let globalQueueWithQos = DispatchQueue.global(qos: .userInitiated)
從任務的履行情況來說,可以分為
這里我們用1張圖,來說解下甚么是串行隊列和并行隊列。
在Swfit 3.0中,創建1個串行/并行隊列
let serialQueue = DispatchQueue(label: "com.leo.serialQueue")
let concurrentQueue = DispatchQueue(label: "com.leo.concurrentQueue",attributes:.concurrent)
async
提交1段任務到隊列,并且立刻返回舉個例子:
我們新建1個方法來摹擬1段很長時間的任務,比如讀1張很大的圖
public func readDataTask(label:String){
NSLog("Start sync task%@",label)
sleep(2)
NSLog("End sync task%@",label)
}
Tips:如果代碼運行在Playground里,記得在最上面加上這兩行。
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
然后,我們來看看在serial和concurrent隊列上,任務的履行情況。
let serialQueue = DispatchQueue(label: "com.leo.serialQueue")
print("Main queue Start")
serialQueue.async {
readDataTask(label: "1")
}
serialQueue.async {
readDataTask(label: "2")
}
print("Main queue End")
會看到Log
Main queue Start
Main queue End
2017-01-04 22:51:40.909 GCD[28376:888938] Start task: 1
2017-01-04 22:51:42.979 GCD[28376:888938] End task: 1
2017-01-04 22:51:42.980 GCD[28376:888938] Start task: 2
2017-01-04 22:51:45.051 GCD[28376:888938] End task: 2
也就是說,任務的履行是
主線程依照順序提交任務1,任務2到
serialQueue
,瞬間履行終了,并沒有被阻塞。
在serialQueue
上先履行任務1,任務1履行終了后再履行任務2.
再來看看concurrent隊列
print("Main queue Start")
let concurrentQueue = DispatchQueue(label: "com.leo.concurrent", attributes: .concurrent)
concurrentQueue.async {
readDataTask(label: "3")
}
concurrentQueue.async {
readDataTask(label: "4")
}
print("Main queue End")
Log以下
Main queue Start
Main queue End
2017-01-04 23:06:36.659 GCD[28642:902085] Start task: 3
2017-01-04 23:06:36.659 GCD[28642:902070] Start task: 4
2017-01-04 23:06:38.660 GCD[28642:902085] End task: 3
2017-01-04 23:06:38.668 GCD[28642:902070] End task: 4
可以看到:
主線程仍然沒有被阻塞。
在concurrentQueue
隊列上,兩個任務依照提交的次序開始,兩個任務并發的履行了。
async
提交1段任務到隊列,并且阻塞當前線程,任務結束后當前線程繼續履行我們把上文的代碼改成sync
let serialQueue = DispatchQueue(label: "com.leo.queue")
print("Main queue Start")
serialQueue.sync {
readDataTask(label: "1")
}
print("Main queue End")
這時候候Log以下
Main queue Start
2017-01-04 23:21:29.422 GCD[28796:912732] Start task: 1
2017-01-04 23:21:31.423 GCD[28796:912732] End task: 1
Main queue End
sync是1個強大但是容易被忽視的函數。使用sync,可以方便的進行線程間同步。但是,有1點要注意,sync容易造成死鎖,這個后文會講到。
QoS的全稱是quality of service。在Swift 3中,它是1個結構體,用來制定隊列或任務的重要性。
作甚重要性呢?就是當資源有限的時候,優先履行哪些任務。這些優先級包括CPU時間,數據IO等等,也包括ipad muiti tasking(兩個App同時在前臺運行)。
通常使用QoS為以下4種
從上到下優先級順次下降。
順次含義以下,
User Interactive
和用戶交相互關,比如動畫等等優先級最高。比如用戶連續拖拽的計算User Initiated
需要立刻的結果,比如push1個ViewController之前的數據計算Utility
可以履行很長時間,再通知用戶結果。比以下載1個文件,給用戶下載進度。Background
用戶不可見,比如在后臺存儲大量數據通常,你需要問自己以下幾個問題
在GCD中,指定QoS有以下兩種方式
方式1,創建1個指定QoS的queue
let backgroundQueue = DispatchQueue(label: "com.leo.backgroundQueu", qos: .background)
backgroundQueue.async {
//在QoS為background下運行
}
方式2,在提交block的時候,指定QoS
queue.async(qos: .background) {
//在QoS為background下運行
}
上文提到的方式,我們都是以block(或叫閉包)的情勢提交任務。DispatchWorkItem
則把任務封裝成了1個對象。
比如,你可以這么使用
let item = DispatchWorkItem {
//任務
}
DispatchQueue.global().async(execute: item)
也能夠在初始化的時候指定更多的參數
let item = DispatchWorkItem(qos: .userInitiated, flags: [.enforceQoS,.assignCurrentContext]) {
//任務
}
其中
DispatchWorkItemFlags
。指定這個任務的配飾信息DispatchWorkItemFlags的參數分為兩組
履行情況
- barrier
- detached
- assignCurrentContext
QoS覆蓋信息
- noQoS //沒有QoS
- inheritQoS //繼承Queue的QoS
- enforceQoS //自己的QoS覆蓋Queue
Tips:Swift 3中,提交1個block的任務,通常也能夠傳入DispatchWorkItem
對象
asyncAfter
和syncAfter
來提交1個延遲履行的任務比如
let deadline = DispatchTime.now() + 2.0
NSLog("Start")
DispatchQueue.global().asyncAfter(deadline: deadline) {
NSLog("End")
}
可以看到,兩秒后打印了End
2017-01-05 22:42:04.781 GCD[1617:36711] Start
2017-01-05 22:42:06.972 GCD[1617:36768] End
延遲履行還支持1種模式DispatchWallTime
let walltime = DispatchWallTime.now() + 2.0
NSLog("Start")
DispatchQueue.global().asyncAfter(wallDeadline: walltime) {
NSLog("End")
}
這里的區分就是
DispatchSource provides an interface for monitoring low-level system objects such as Mach ports, Unix descriptors, Unix signals, and VFS nodes for activity and submitting event handlers to dispatch queues for asynchronous processing when such activity occurs.
DispatchSource提供了1組接口,用來提交hander監測底層的事件,這些事件包括Mach ports,Unix descriptors,Unix signals,VFS nodes。
這1組事件包括:
Protocol | 含義 |
---|---|
DispatchSourceUserDataAdd | 用戶自定義數據add |
DispatchSourceUserDataOr | 用戶自定義數據Or |
DispatchSourceMachSend | Mach端口發送 |
DispatchSourceMachReceive | Mach端口接收 |
DispatchSourceMemoryPressure | 內存壓力 |
DispatchSourceProcess | 進程事件 |
DispatchSourceRead | 讀數據 |
DispatchSourceSignal | 信號 |
DispatchSourceTimer | 定時器 |
DispatchSourceFileSystemObject | 文件系統 |
DispatchSourceWrite | 寫數據 |
本文只會觸及到timer和userData,其余的平實開發幾近用不到。
Tips:
DispatchSource
這個class很好的體現了Swift是1門面向協議的語言。這個類是1個工廠類,用來實現各種source。比如DispatchSourceTimer(本身是個協議)表示1個定時器。
基礎協議,所有的用到的DispatchSource
都實現了這個協議。這個協議的提供了公共的方法和屬性:
由于不同的source是用到的屬性和方法不1樣,這里只列出幾個公共的方法
- activate //激活
- suspend //掛起
- resume //繼續
- cancel //取消(異步的取消,會保證當前eventHander履行完)
- setEventHandler //事件處理邏輯
- setCancelHandler //取消時候的清算邏輯
在Swift 3中,可以方便的用GCD創建1個Timer(新特性)。DispatchSourceTimer本身是1個協議。
比如,寫1個timer,1秒后履行,然后10秒后自動取消,允許10毫秒的誤差
PlaygroundPage.current.needsIndefiniteExecution = true
public let timer = DispatchSource.makeTimerSource()
timer.setEventHandler {
//這里要注意循環援用,[weak self] in
print("Timer fired at \(NSDate())")
}
timer.setCancelHandler {
print("Timer canceled at \(NSDate())" )
}
timer.scheduleRepeating(deadline: .now() + .seconds(1), interval: 2.0, leeway: .microseconds(10))
print("Timer resume at \(NSDate())")
timer.resume()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10), execute:{
timer.cancel()
})
其中參數
deadline
表示開始時間leeway
表示能夠容忍的誤差。會看到Log
Timer resume at 2017-01-06 04:30:22 +0000
Timer fired at 2017-01-06 04:30:23 +0000
Timer fired at 2017-01-06 04:30:25 +0000
Timer fired at 2017-01-06 04:30:27 +0000
Timer fired at 2017-01-06 04:30:29 +0000
Timer fired at 2017-01-06 04:30:31 +0000
Timer canceled at 2017-01-06 04:30:32 +0000
DispatchSourceTimer也支持只調用1次。
func scheduleOneshot(deadline: DispatchTime, leeway: DispatchTimeInterval = default)
DispatchGroup用來管理1組任務的履行,然后監聽憑務都完成的事件。比如,多個網絡要求同時發出去,等網絡要求都完成后reload UI。
我們先來實現多網絡要求同步的模型。
首先寫1個函數,摹擬異步網絡要求
public func networkTask(label:String, cost:UInt32, complete:@escaping ()->()){
NSLog("Start network Task task%@",label)
DispatchQueue.global().async {
sleep(cost)
NSLog("End networkTask task%@",label)
DispatchQueue.main.async {
complete()
}
}
}
這個函數有3個參數
然后,我們摹擬兩個耗時2秒和4秒的網絡要求
PlaygroundPage.current.needsIndefiniteExecution = true
let group = DispatchGroup()
group.enter()
networkTask(label: "1", cost: 2, complete: {
group.leave()
})
group.enter()
networkTask(label: "2", cost: 4, complete: {
group.leave()
})
group.notify(queue: .main, execute:{
print("All network is done")
})
然后,看到Log
2017-01-06 12:42:13.559 Dispatch[4590:109934] Start network Task task1
2017-01-06 12:42:13.559 Dispatch[4590:109934] Start network Task task2
2017-01-06 12:42:15.631 Dispatch[4590:109986] End networkTask task1
2017-01-06 12:42:17.635 Dispatch[4590:109983] End networkTask task2
All network is done
DispatchGroup支持阻塞當前線程,等待履行結果
PlaygroundPage.current.needsIndefiniteExecution = true
NSLog("Group created")
let group = DispatchGroup()
group.enter()
networkTask(label: "1", cost: 2, complete: {
group.leave()
})
group.enter()
networkTask(label: "2", cost: 4, complete: {
group.leave()
})
NSLog("Before wait")
//在這個點,等待3秒鐘
group.wait(timeout:.now() + .seconds(3))
NSLog("After wait")
group.notify(queue: .main, execute:{
print("All network is done")
})
比如,我們在提交完第2個任務后,等待3秒中。
PlaygroundPage.current.needsIndefiniteExecution = true
NSLog("Group created")
let group = DispatchGroup()
group.enter()
networkTask(label: "1", cost: 2, complete: {
group.leave()
})
group.enter()
networkTask(label: "2", cost: 4, complete: {
group.leave()
})
NSLog("Before wait")
//在這個點,等待3秒鐘
group.wait(timeout:.now() + .seconds(3))
NSLog("After wait")
group.notify(queue: .main, execute:{
print("All network is done")
})
會看到log
2017-01-06 12:49:49.014 Dispatch[4709:118164] Group created
2017-01-06 12:49:49.021 Dispatch[4709:118164] Start network Task task1
2017-01-06 12:49:49.021 Dispatch[4709:118164] Start network Task task2
2017-01-06 12:49:49.021 Dispatch[4709:118164] Before wait
2017-01-06 12:49:51.095 Dispatch[4709:118208] End networkTask task1
2017-01-06 12:49:52.022 Dispatch[4709:118164] After wait
2017-01-06 12:49:53.095 Dispatch[4709:118210] End networkTask task2
All network is done
DispatchSemaphore provides an efficient implementation of a traditional counting semaphore, which can be used to control access to a resource across multiple execution contexts.
DispatchSemaphore是傳統計數信號量的封裝,用來控制資源被多任務訪問的情況。
簡單來講,如果我只有兩個usb端口,如果來了3個usb要求的話,那末第3個就要等待,等待有1個空出來的嘶吼,第3個要求才會繼續履行。
我們來摹擬這1情況:
public func usbTask(label:String, cost:UInt32, complete:@escaping ()->()){
NSLog("Start usb task%@",label)
sleep(cost)
NSLog("End usb task%@",label)
complete()
}
很簡單,打印log,休眠,然后調用complete給出回調。
PlaygroundPage.current.needsIndefiniteExecution = true
let semaphore = DispatchSemaphore(value: 2)
let queue = DispatchQueue(label: "com.leo.concurrentQueue", qos: .default, attributes: .concurrent)
queue.async {
semaphore.wait()
usbTask(label: "1", cost: 2, complete: {
semaphore.signal()
})
}
queue.async {
semaphore.wait()
usbTask(label: "2", cost: 2, complete: {
semaphore.signal()
})
}
queue.async {
semaphore.wait()
usbTask(label: "3", cost: 1, complete: {
semaphore.signal()
})
}
Log
2017-01-06 15:03:09.264 Dispatch[5711:162205] Start usb task2
2017-01-06 15:03:09.264 Dispatch[5711:162204] Start usb task1
2017-01-06 15:03:11.338 Dispatch[5711:162205] End usb task2
2017-01-06 15:03:11.338 Dispatch[5711:162204] End usb task1
2017-01-06 15:03:11.339 Dispatch[5711:162219] Start usb task3
2017-01-06 15:03:12.411 Dispatch[5711:162219] End usb task3
Tips:在serial queue上使用信號量要注意死鎖的問題。感興趣的同學可以把上述代碼的
queue
改成serial的,看看效果。
barrier翻譯過來就是屏障。在1個并行queue里,很多時候,我們提交1個新的任務需要這樣做。
- queue里之前的任務履行完了新任務才開始
- 新任務開始后提交的任務都要等待新任務履行終了才能繼續履行
以barrier flag提交的任務能夠保證其在并行隊列履行的時候,是唯1的1個任務。(只對自己創建的隊列有效,對gloablQueue無效)
典型的場景就是往NSMutableArray
里addObject
。
我們寫個例子來看看效果
PlaygroundPage.current.needsIndefiniteExecution = true
let concurrentQueue = DispatchQueue(label: "com.leo.concurrent", attributes: .concurrent)
concurrentQueue.async {
readDataTask(label: "1", cost: 3)
}
concurrentQueue.async {
readDataTask(label: "2", cost: 3)
}
concurrentQueue.async(flags: .barrier, execute: {
NSLog("Task from barrier 1 begin")
sleep(3)
NSLog("Task from barrier 1 end")
})
concurrentQueue.async {
readDataTask(label: "2", cost: 3)
}
然后,看到Log
2017-01-06 17:14:19.690 Dispatch[15609:245546] Start data task1
2017-01-06 17:14:19.690 Dispatch[15609:245542] Start data task2
2017-01-06 17:14:22.763 Dispatch[15609:245546] End data task1
2017-01-06 17:14:22.763 Dispatch[15609:245542] End data task2
2017-01-06 17:14:22.764 Dispatch[15609:245546] Task from barrier 1 begin
2017-01-06 17:14:25.839 Dispatch[15609:245546] Task from barrier 1 end
2017-01-06 17:14:25.839 Dispatch[15609:245546] Start data task3
2017-01-06 17:14:28.913 Dispatch[15609:245546] End data task3
履行的效果就是:barrier任務提交后,等待前面所有的任務都完成了才履行本身。barrier任務履行完了后,再履行后續履行的任務。
用1張圖來表示:
DispatchSource
中UserData部份也是強有力的工具,這部份包括兩個協議,兩個協議都是用來合并數據的變化,只不過1個是依照+(加)
的方式,1個是依照|(位與)
的方式。
在使用這兩種Source的時候,GCD會幫助我們自動的將這些改變合并,然后在適當的時候(target queue空閑)的時候,去回調
EventHandler
,從而避免了頻繁的回調致使CPU占用過量。
比如,對DispatchSourceUserDataAdd
你可以這么使用,
let userData = DispatchSource.makeUserDataAddSource()
var globalData:UInt = 0
userData.setEventHandler {
let pendingData = userData.data
globalData = globalData + pendingData
print("Add \(pendingData) to global and current global is \(globalData)")
}
userData.resume()
let serialQueue = DispatchQueue(label: "com")
serialQueue.async {
for var index in 1...1000{
userData.add(data: 1)
}
for var index in 1...1000{
userData.add(data: 1)
}
}
然后,你會發現Log
Add 32 to global and current global is 32
Add 1321 to global and current global is 1353
Add 617 to global and current global is 1970
Add 30 to global and current global is 2000
通常,在多線程同時會對1個變量(比如NSMutableArray
)進行讀寫的時候,我們需要斟酌到線程的同步。舉個例子:比如線程1在對NSMutableArray進行addObject的時候,線程2如果也想addObject
,那末它必須等到線程1履行終了后才可以履行。
實現這類同步有很多種機制:
比如用互斥鎖:
let lock = NSLock()
lock.lock()
//Do something
lock.unlock()
使用鎖有1個不好的地方就是:lock
和unlock
要配對使用,不然極容易鎖住線程,沒有釋放掉。
使用GCD,隊列同步有另外1種方式 - sync
,講屬性的訪問同步到1個queue上去,就可以保證在多線程同時訪問的時候,線程安全。
class MyData{
private var privateData:Int = 0
private let dataQueue = DispatchQueue(label: "com.leo.dataQueue")
var data:Int{
get{
return dataQueue.sync{ privateData }
}
set{
dataQueue.sync { privateData = newValue}
}
}
}
GCD對線程進行了很好的封裝,但是依然又可能出現死鎖。所謂死鎖,就是線程之間相互等待對方履行,才能繼續履行,致使進入了1個死循環的狀態。
最簡單的死鎖,在main線程上sync自己。
DispatchQueue.main.sync {
print("You can not see this log")
}
緣由也比較好理解,在調用sync的時候,main隊列被阻塞,等到代碼塊履行終了才會繼續履行。由于main被阻塞,就致使了代碼塊沒法履行,進而構成死鎖。
還有1種死鎖,簡單的代碼以下
queueA.sync {
queueB.sync {
queueC.sync {
queueA.sync {
}
}
}
}
死鎖的緣由很簡單,構成了1個相互阻塞的環。