iOS多線程深入解析
必要的概念
進(jìn)程/線程
進(jìn)程:進(jìn)程指在系統(tǒng)中能獨立運行并作為資源分配的基本單位,它是由一組機器指令、數(shù)據(jù)和堆棧等組成的,是一個能獨立運行的活動實體。
線程:線程是進(jìn)程的基本執(zhí)行單元,一個進(jìn)程(程序)的所有任務(wù)都在線程中執(zhí)行。
操作系統(tǒng)引入進(jìn)程的目的:為了使多個程序能并發(fā)執(zhí)行,以提高資源的利用率和系統(tǒng)的吞吐量。
操作系統(tǒng)引入線程的目的:在操作系統(tǒng)中再引入線程,則是為了減少程序在并發(fā)執(zhí)行時所付出的時空開銷,使OS具有更好的并發(fā)性。多線程技術(shù)可以提高程序的執(zhí)行效率。
在引入線程的OS中,通常把進(jìn)程作為資源分配的基本單位,而把線程作為獨立運行和獨立調(diào)度的基本單位。
同步/異步
同步:多個任務(wù)情況下,一個任務(wù)A執(zhí)行結(jié)束,才可以執(zhí)行另一個任務(wù)B。
異步:多個任務(wù)情況下,一個任務(wù)A正在執(zhí)行,同時可以執(zhí)行另一個任務(wù)B。任務(wù)B不用等待任務(wù)A結(jié)束才執(zhí)行。存在多條線程。
并行/并發(fā)
并行:指兩個或多個事件在同一時刻發(fā)生。多核CUP同時開啟多條線程供多個任務(wù)同時執(zhí)行,互不干擾。
并發(fā):指兩個或多個事件在同一時間間隔內(nèi)發(fā)生??梢栽谀硹l線程和其他線程之間反復(fù)多次進(jìn)行上下文切換,看上去就好像一個CPU能夠并且執(zhí)行多個線程一樣。其實是偽異步。
線程間通信
在1個進(jìn)程中,線程往往不是孤立存在的,多個線程之間需要經(jīng)常進(jìn)行通信
線程間通信的體現(xiàn):
- 1個線程傳遞數(shù)據(jù)給另1個線程
- 在1個線程中執(zhí)行完特定任務(wù)后,轉(zhuǎn)到另1個線程繼續(xù)執(zhí)行任務(wù)
多線程概念
多線程是指在軟件或硬件上實現(xiàn)多個線程并發(fā)執(zhí)行的技術(shù)。通俗講就是在同步或異步的情況下,開辟新線程,進(jìn)行線程間的切換,以及對線程進(jìn)行合理的調(diào)度,做到優(yōu)化提升程序性能的目的。
多線程的優(yōu)點
- 能適當(dāng)提高程序的執(zhí)行效率
- 能適當(dāng)提高資源利用率(CPU、內(nèi)存利用率)
- 避免在處理耗時任務(wù)時造成主線程阻塞
多線程的缺點
- 開啟線程需要占用一定的內(nèi)存空間,如果開啟大量的線程,會占用大量的內(nèi)存空間,降低程序的性能
- 線程越多,CPU在調(diào)度線程上的開銷就越大
- 可能會導(dǎo)致多個線程相互持續(xù)等待[死鎖]
- 程序設(shè)計更加復(fù)雜:比如線程之間的通信、多線程之間的數(shù)據(jù)競爭
GCD(Grand Central Dispatch)
Dispatch會自動的根據(jù)CPU的使用情況,創(chuàng)建線程來執(zhí)行任務(wù),并且自動的運行到多核上,提高程序的運行效率。對于開發(fā)者來說,在GCD層面是沒有線程的概念的,只有隊列(queue)。任務(wù)都是以block的方式提交到隊列上,然后GCD會自動的創(chuàng)建線程池去執(zhí)行這些任務(wù)。
GCD的優(yōu)點:
- GCD是蘋果公司為多核的并行運算提出的解決方案
- GCD會自動利用更多的CPU內(nèi)核(比如雙核、四核)
- GCD會自動管理線程的生命周期(創(chuàng)建線程、調(diào)度任務(wù)、銷毀線程)
- 程序員只需要告訴GCD想要執(zhí)行什么任務(wù),不需要編寫任何線程管理代碼
GCD中有兩個核心概念
任務(wù) block:執(zhí)行什么操作
隊列 queue:用來存放任務(wù)
GCD的使用就兩個步驟
- 定制任務(wù),確定想做的事情
- 將任務(wù)添加到隊列中,
GCD會自動將隊列中的任務(wù)取出,放到對應(yīng)的線程中執(zhí)行。任務(wù)的取出遵循隊列的FIFO原則:先進(jìn)先出,后進(jìn)后出。
GCD中有兩個執(zhí)行任務(wù)的函數(shù)
-
同步執(zhí)行任務(wù)(sync)
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);同步任務(wù)會阻塞當(dāng)前線程,然后把
Block中的任務(wù)放到指定的隊列中執(zhí)行,只有等到Block中的任務(wù)完成后才會讓當(dāng)前線程繼續(xù)往下運行。sync是一個強大但是容易被忽視的函數(shù)。使用sync,可以方便的進(jìn)行線程間同步。但是,有一點要注意,sync容易造成死鎖。
-
異步執(zhí)行任務(wù)(async)
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);異步任務(wù)會再開辟一個線程,當(dāng)前線程繼續(xù)往下走,新線程去執(zhí)行
block里的任務(wù)。
GCD的隊列可以分為兩大類型
-
并行隊列(Concurrent Dispatch Queue):
- 可以讓多個任務(wù)并發(fā)(同時)執(zhí)行(自動開啟多個線程同時執(zhí)行任務(wù))
- 并行功能只有在異步(dispatch_async)函數(shù)下才有效
放到并行隊列的任務(wù),如果是異步執(zhí)行,
GCD也會FIFO的取出來,但不同的是,它取出來一個就會放到別的線程,然后再取出來一個又放到另一個的線程。這樣由于取的動作很快,忽略不計,看起來,所有的任務(wù)都是一起執(zhí)行的。不過需要注意,GCD會根據(jù)系統(tǒng)資源控制并行的數(shù)量,所以如果任務(wù)很多,它并不會讓所有任務(wù)同時執(zhí)行。
-
串行隊列(Serial Dispatch Queue):
讓任務(wù)一個接著一個地執(zhí)行(一個任務(wù)執(zhí)行完畢后,再執(zhí)行下一個任務(wù))
同步執(zhí)行 異步執(zhí)行 串行隊列 當(dāng)前線程,一個一個執(zhí)行 其他線程,一個一個執(zhí)行 并發(fā)隊列 當(dāng)前線程,一個一個執(zhí)行 開很多線程,一起執(zhí)行
Swift4 GCD 使用
DispatchQueue
最簡單的,可以按照以下方式初始化一個隊列
//這里的名字能夠方便開發(fā)者進(jìn)行Debug
let queue = DispatchQueue(label: "com.geselle.demoQueue")
這樣初始化的隊列是一個默認(rèn)配置的隊列,也可以顯式的指明對列的其他屬性
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)
這里,我們來一個參數(shù)分析他們的作用
-
label: 隊列的標(biāo)識符,方便調(diào)試 -
qos: 隊列的quality of service。用來指明隊列的“重要性”,后文會詳細(xì)講到。 -
attributes: 隊列的屬性。類型是DispatchQueue.Attributes,是一個結(jié)構(gòu)體,遵循了協(xié)議OptionSet。意味著你可以這樣傳入第一個參數(shù)[.option1,.option2]。- 默認(rèn):隊列是串行的。
-
.concurrent:隊列為并行的。 -
.initiallyInactive:則隊列任務(wù)不會自動執(zhí)行,需要開發(fā)者手動觸發(fā)。
-
autoreleaseFrequency: 顧名思義,自動釋放頻率。有些隊列是會在執(zhí)行完任務(wù)后自動釋放的,有些比如Timer等是不會自動釋放的,是需要手動釋放。
隊列分類
- 系統(tǒng)創(chuàng)建的隊列
- 主隊列(對應(yīng)主線程)
- 全局隊列
- 用戶創(chuàng)建的隊列
// 獲取系統(tǒng)隊列
let mainQueue = DispatchQueue.main
let globalQueue = DispatchQueue.global()
let globalQueueWithQos = DispatchQueue.global(qos: .userInitiated)
// 創(chuàng)建串行隊列
let serialQueue = DispatchQueue(label: "com.geselle.serialQueue")
// 創(chuàng)建并行隊列
let concurrentQueue = DispatchQueue(label: "com.geselle.concurrentQueue",attributes:.concurrent)
// 創(chuàng)建并行隊列,并手動觸發(fā)
let concurrentQueue2 = DispatchQueue(label:"com.geselle.concurrentQueue2", qos: .utility,attributes[.concurrent,.initiallyInactive])
//手動觸發(fā)
if let queue = inactiveQueue {
queue.activate()
}
suspend / resume
Suspend可以掛起一個線程,就是把這個線程暫停了,它占著資源,但不運行。
Resume可以繼續(xù)掛起的線程,讓這個線程繼續(xù)執(zhí)行下去。
concurrentQueue.resume()
concurrentQueue.suspend()
QoS(quality of service)
QoS的全稱是quality of service。在Swift 3中,它是一個結(jié)構(gòu)體,用來制定隊列或者任務(wù)的重要性。
何為重要性呢?就是當(dāng)資源有限的時候,優(yōu)先執(zhí)行哪些任務(wù)。這些優(yōu)先級包括 CPU 時間,數(shù)據(jù) IO 等等,也包括 ipad muiti tasking(兩個App同時在前臺運行)。
通常使用QoS為以下四種,從上到下優(yōu)先級依次降低。
-
User Interactive: 和用戶交互相關(guān),比如動畫等等優(yōu)先級最高。比如用戶連續(xù)拖拽的計算 -
User Initiated: 需要立刻的結(jié)果,比如push一個ViewController之前的數(shù)據(jù)計算 -
Utility: 可以執(zhí)行很長時間,再通知用戶結(jié)果。比如下載一個文件,給用戶下載進(jìn)度。 -
Background: 用戶不可見,比如在后臺存儲大量數(shù)據(jù)
通常,你需要問自己以下幾個問題
- 這個任務(wù)是用戶可見的嗎?
- 這個任務(wù)和用戶交互有關(guān)嗎?
- 這個任務(wù)的執(zhí)行時間有多少?
- 這個任務(wù)的最終結(jié)果和UI有關(guān)系嗎?
在GCD中,指定QoS有以下兩種方式
方式一:創(chuàng)建一個指定QoS的queue
let backgroundQueue = DispatchQueue(label: "com.geselle.backgroundQueue", qos: .background)
backgroundQueue.async {
//在QoS為background下運行
}
方式二:在提交block的時候,指定QoS
queue.async(qos: .background) {
//在QoS為background下運行
}
DispatchGroup
DispatchGroup用來管理一組任務(wù)的執(zhí)行,然后監(jiān)聽任務(wù)都完成的事件。比如,多個網(wǎng)絡(luò)請求同時發(fā)出去,等網(wǎng)絡(luò)請求都完成后reload UI。
let group = DispatchGroup()
let queueBook = DispatchQueue(label: "book")
print("start networkTask task 1")
queueBook.async(group: group) {
sleep(2)
print("End networkTask task 1")
}
let queueVideo = DispatchQueue(label: "video")
print("start networkTask task 2")
queueVideo.async(group: group) {
sleep(2)
print("End networkTask task 2")
}
group.notify(queue: DispatchQueue.main) {
print("all task done")
}
group.notify會等group里的所有任務(wù)全部完成以后才會執(zhí)行(不管是同步任務(wù)還是異步任務(wù))。
Group.enter / Group.leave
/*
首先寫一個函數(shù),模擬異步網(wǎng)絡(luò)請求
這個函數(shù)有三個參數(shù)
* label 表示id
* cost 表示時間消耗
* complete 表示任務(wù)完成后的回調(diào)
*/
public func networkTask(label:String, cost:UInt32, complete:@escaping ()->()){
print("Start network Task task%@",label)
DispatchQueue.global().async {
sleep(cost)
print("End networkTask task%@",label)
DispatchQueue.main.async {
complete()
}
}
}
// 我們模擬兩個耗時2秒和4秒的網(wǎng)絡(luò)請求
print("Group created")
let group = DispatchGroup()
group.enter()
networkTask(label: "1", cost: 2, complete: {
group.leave()
})
group.enter()
networkTask(label: "2", cost: 2, complete: {
group.leave()
})
group.wait(timeout: .now() + .seconds(4))
group.notify(queue: .main, execute:{
print("All network is done")
})
Group.wait
DispatchGroup支持阻塞當(dāng)前線程,等待執(zhí)行結(jié)果。
//在這個點,等待三秒鐘
group.wait(timeout:.now() + .seconds(3))
DispatchWorkItem
上文提到的方式,我們都是以block(或者叫閉包)的形式提交任務(wù)。DispatchWorkItem則把任務(wù)封裝成了一個對象。
比如,你可以這么使用
let item = DispatchWorkItem {
//任務(wù)
}
DispatchQueue.global().async(execute: item)
也可以在初始化的時候指定更多的參數(shù)
let item = DispatchWorkItem(qos: .userInitiated, flags: [.enforceQoS,.assignCurrentContext]) {
//任務(wù)
}
* 第一個參數(shù)表示 QoS。
* 第二個參數(shù)類型為 DispatchWorkItemFlags。指定這個任務(wù)的配飾信息
* 第三個參數(shù)則是實際的任務(wù) block
DispatchWorkItemFlags的參數(shù)分為兩組
-
執(zhí)行情況
- barrier
- detached
- assignCurrentContext
-
QoS覆蓋信息
- noQoS //沒有 QoS
- inheritQoS //繼承 Queue 的 QoS
- enforceQoS //自己的 QoS 覆蓋 Queue
after(延遲執(zhí)行)
GCD可以通過asyncAfter來提交一個延遲執(zhí)行的任務(wù)
比如
let deadline = DispatchTime.now() + 2.0
print("Start")
DispatchQueue.global().asyncAfter(deadline: deadline) {
print("End")
}
延遲執(zhí)行還支持一種模式DispatchWallTime
let walltime = DispatchWallTime.now() + 2.0
print("Start")
DispatchQueue.global().asyncAfter(wallDeadline: walltime) {
print("End")
}
這里的區(qū)別就是
-
DispatchTime的精度是納秒 -
DispatchWallTime的精度是微秒
Synchronization 同步
通常,在多線程同時會對一個變量(比如NSMutableArray)進(jìn)行讀寫的時候,我們需要考慮到線程的同步。舉個例子:比如線程一在對NSMutableArray進(jìn)行addObject的時候,線程二如果也想addObject,那么它必須等到線程一執(zhí)行完畢后才可以執(zhí)行。
實現(xiàn)這種同步有很多種機制
NSLock 互斥鎖
let lock = NSLock()
lock.lock()
//Do something
lock.unlock()
使用鎖有一個不好的地方就是:lock和unlock要配對使用,不然極容易鎖住線程,沒有釋放掉。
sync 同步函數(shù)
使用GCD,隊列同步有另外一種方式- sync,講屬性的訪問同步到一個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}
}
}
}
Barrier 線程阻斷
假設(shè)我們有一個并發(fā)的隊列用來讀寫一個數(shù)據(jù)對象。如果這個隊列里的操作是讀的,那么可以多個同時進(jìn)行。如果有寫的操作,則必須保證在執(zhí)行寫入操作時,不會有讀取操作在執(zhí)行,必須等待寫入完成后才能讀取,否則就可能會出現(xiàn)讀到的數(shù)據(jù)不對。這個時候我們會用到 Barrier。
以barrier flag提交的任務(wù)能夠保證其在并行隊列執(zhí)行的時候,是唯一的一個任務(wù)。(只對自己創(chuàng)建的隊列有效,對gloablQueue無效)
我們寫個例子來看看效果
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
執(zhí)行的效果就是:barrier任務(wù)提交后,等待前面所有的任務(wù)都完成了才執(zhí)行自身。barrier任務(wù)執(zhí)行完了后,再執(zhí)行后續(xù)執(zhí)行的任務(wù)。
Semaphore 信號量
DispatchSemaphore是傳統(tǒng)計數(shù)信號量的封裝,用來控制資源被多任務(wù)訪問的情況。
簡單來說,如果我只有兩個usb端口,如果來了三個usb請求的話,那么第3個就要等待,等待有一個空出來的時候,第三個請求才會繼續(xù)執(zhí)行。
我們來模擬這一情況:
public func usbTask(label:String, cost:UInt32, complete:@escaping ()->()){
print("Start usb task%@",label)
sleep(cost)
print("End usb task%@",label)
complete()
}
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上使用信號量要注意死鎖的問題。感興趣的同學(xué)可以把上述代碼的queue改成serial的,看看效果。