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ā)生∷蹙伲可以在某條線程和其他線程之間反復(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
的慢蜓,看看效果亚再。