引言
今天從Books中翻出了“沉淀”已久的關(guān)于并發(fā)編程書(shū),讀完之后,感受頗多,有一些不確定的知識(shí)點(diǎn)更加清晰了。在此,結(jié)合自己的開(kāi)發(fā)經(jīng)驗(yàn),做一個(gè)總結(jié)。主要從兩個(gè)方面入手:GCD和Operation,也是目前比較主流的實(shí)現(xiàn)并發(fā)的方式。
GCD 和 Operation的區(qū)別和抉擇
GCD是基于libdispatch實(shí)現(xiàn)的,目的是為了降低并發(fā)的成本;最大的特點(diǎn)是大量使用了block,使多線程的實(shí)現(xiàn)變得簡(jiǎn)單。Operation是基于GCD的封裝,更具有封裝性,具備GCD不能實(shí)現(xiàn)的功能,比如cancel任務(wù)。
如何選擇?
選用GCD:簡(jiǎn)單任務(wù),且代碼不會(huì)被重用;
選用Operation:代碼適合封裝起來(lái)重用;多層異步嵌套(如異步A回調(diào)B,B回調(diào)C...如果用GCD會(huì)造成多層block嵌套,可讀性太差);有cancel操作。
不要混淆隊(duì)列和Thread
先來(lái)看一個(gè)面試經(jīng)常遇到的考題:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{ // 這個(gè)地方會(huì)阻塞主線程
print("hello");
});
首先隊(duì)列不是線程,隊(duì)列是一個(gè)FIFO的數(shù)據(jù)結(jié)構(gòu);創(chuàng)建一個(gè)隊(duì)列,系統(tǒng)會(huì)根據(jù)系統(tǒng)的使用情況創(chuàng)建一個(gè)或者多個(gè)線程來(lái)配合這個(gè)隊(duì)列工作。線程是實(shí)實(shí)在在運(yùn)行邏輯的。你可以抽象成這個(gè)樣子:

根據(jù)圖來(lái)分析: 主線程執(zhí)行主隊(duì)列中的print("Hello")操作,而且是同步的,此時(shí)主線程等待,主隊(duì)列任務(wù)開(kāi)始執(zhí)行,但是主隊(duì)列的Task需要在主線程完成,這樣就出現(xiàn)了兩者互相等待的情況,所以造成死鎖。所以請(qǐng)一定記住“隊(duì)列不等于線程”
GCD常見(jiàn)用法
-
創(chuàng)建同步隊(duì)列
let label = "com.piplab.queue.identifer" let queue = DispatchQueue(label: label)是隊(duì)列的唯一標(biāo)識(shí) ,當(dāng)隊(duì)列創(chuàng)建后,OS會(huì)選擇性的為隊(duì)列創(chuàng)建或分配一個(gè)或多個(gè)線程;如果有可復(fù)用的線程,就復(fù)用線程;否則,將根據(jù)情況創(chuàng)建。
主隊(duì)列比較特殊,App啟動(dòng)時(shí)創(chuàng)建,并且是用來(lái)處理UI相關(guān)操作的同步隊(duì)列。切記永遠(yuǎn)不要同步主隊(duì)列進(jìn)行一些除UI相關(guān)的耗時(shí)操作。
-
創(chuàng)建并發(fā)隊(duì)列
let label = "com.piplab.queue.concurrent" let queue = DispatchQueue(label: label, attributes: .current)除了自定義外,系統(tǒng)已經(jīng)預(yù)定義了6種不同優(yōu)先級(jí)的并發(fā)?隊(duì)列,下面會(huì)講到。
-
隊(duì)列優(yōu)先級(jí)
按照優(yōu)先級(jí)從高到低依次是:
-
.userInteractive: 一般用于用戶交互,需要快速響應(yīng)的情況。 -
.userInitiated: 一般用戶用戶交互之后,需要迅速異步操作的情況,比如用戶需要讀取數(shù)據(jù)庫(kù),需要快速響應(yīng),讀取數(shù)據(jù)。 -
.default: 系統(tǒng)默認(rèn)優(yōu)先級(jí)。不要直接使用,否則會(huì)出現(xiàn)不必要的錯(cuò)誤 -
.utility:一般用于進(jìn)度條,IO,網(wǎng)絡(luò)請(qǐng)求等情況。系統(tǒng)會(huì)根據(jù)電池情況,平衡響應(yīng)頻率 -
.background: 一般用戶用戶不需要感知的情況。 -
.unspecified: 不建議使用。
這六種優(yōu)先級(jí)并發(fā)隊(duì)列,系統(tǒng)都有預(yù)定義。但是并不意味著我們不能創(chuàng)建不同優(yōu)先級(jí)的隊(duì)列。
let label = "com.piplab.quality" let queue = DispatchQueue(label: label, qos: .userInteractive, attributes: .current)注意: Queue的優(yōu)先級(jí)并不是一味不變的。如果將一個(gè)高優(yōu)先級(jí)的Task交給一個(gè)低優(yōu)先級(jí)的Queue,那么Queue的優(yōu)先級(jí)會(huì)跟著提升,并且Queue中的其他Task的優(yōu)先級(jí)也會(huì)跟著提升
-
-
派發(fā)任務(wù)
DispatchQueue.global(qos: .utility).async { [weak self] in guard let self = self else { return } // do something // Switch back to the main queue to // update your UI DispatchQueue.main.async { self.textLabel.text = "New articles available!" } }雖然在block中不聲明
[weak self]也不會(huì)造成循環(huán)引用,但是會(huì)延長(zhǎng)self的聲明周期,直到block執(zhí)行完畢。在切換到主隊(duì)列時(shí),盡量做比較少的工作。 -
DispatchGroup
適用于“當(dāng)一組任務(wù)完成后,再執(zhí)行特定的任務(wù),組任務(wù)并發(fā)執(zhí)行”。
let group = DispatchGroup() someQueue.async(group: group) { ... your work ... } //任務(wù)1 someQueue.async(group: group) { ... more work .... } //任務(wù)2 someOtherQueue.async(group: group) { ... other work ... } //任務(wù)3 group.notify(queue: DispatchQueue.main) { [weak self] in self?.textLabel.text = "All jobs have completed" //任務(wù)4 }值的注意的是一個(gè)group里的Task,可以分配到不同的Queue。 任務(wù)4會(huì)在任務(wù)1,2,3 都執(zhí)行完之后再執(zhí)行,如果任務(wù)1,2,3中有一直執(zhí)行不完,那么任務(wù)4是不會(huì)執(zhí)行的。
如果不能等待所有的任務(wù)執(zhí)行完,還可以用以下方式實(shí)現(xiàn):
let group = DispatchGroup() someQueue.async(group: group) { ... } someQueue.async(group: group) { ... } someOtherQueue.async(group: group) { ... } if group.wait(timeout: .now() + 60) == .timedOut { print("The jobs didn't finish in 60 seconds") } else { print ("All jobs have complete") }切記不要在主線程調(diào)用
group.wait方法.對(duì)于多層異步嵌套的情況,group還有另外一種實(shí)現(xiàn)方式:
queue.dispatch(group: group) { // count is 1 group.enter() // count is 2 someAsyncMethod { defer { group.leave() } // Perform your work here, // count goes back to 1 once complete } } -
信號(hào)量Semaphores
簡(jiǎn)單的理解信號(hào)量 就是一個(gè)計(jì)數(shù)器,每次使用時(shí) -1 ,為0時(shí),代表資源不夠,線程等待,釋放時(shí) +1 。一個(gè)簡(jiǎn)單的例子: 批量下載圖片,最多4個(gè)線程同時(shí)下載,那么4可以定義為一個(gè)信號(hào)量。
let semaphore = DispatchSemaphore(value: 4) semaphore.wait() //信號(hào)量-1,如果為0,則等待 semaphore.signal() //信號(hào)量 +1 -
并發(fā)三大難題
- 資源競(jìng)爭(zhēng):在多個(gè)線程“同時(shí)寫(xiě)” 的情況,容易出現(xiàn)錯(cuò)誤。
2.png
- 資源競(jìng)爭(zhēng):在多個(gè)線程“同時(shí)寫(xiě)” 的情況,容易出現(xiàn)錯(cuò)誤。
假設(shè)有一個(gè)值value為1,別用兩個(gè)線程各使value的值+1,正確結(jié)果應(yīng)該是3,但實(shí)際效果是:value的值為1,第一個(gè)時(shí)鐘,Thread1讀取的值是1;第二個(gè)時(shí)鐘Thread1將value的值修改為2,因?yàn)門(mén)hread1還沒(méi)有寫(xiě)回到value中,所以Thread2讀取的值是1,第三個(gè)時(shí)鐘Thread1將2寫(xiě)回到value中,thread2將value值+1修改為2,第四個(gè)時(shí)鐘,Thread2將2寫(xiě)回到value中,這樣結(jié)果為2,所以是錯(cuò)誤的。正確的做法是將“+1”操作和寫(xiě)操作同步執(zhí)行。
`serialQueue.async{ value +1 ,write()}`
下面說(shuō)一下并發(fā)情況下變量訪問(wèn)安全措施(這是另外一個(gè)問(wèn)題,跟上面無(wú)關(guān)哦)。
當(dāng)多個(gè)線程訪問(wèn)同意變量時(shí),為了避免讀的同時(shí)寫(xiě)的問(wèn)題,如圖:

實(shí)現(xiàn)如下:
```swift
private let threadSafeCountQueue = DispatchQueue(label: "...", attributes: .concurrent)
private var _count = 0
public var count: Int {
get {
return threadSafeCountQueue.sync {
return _count
}
}
set {
threadSafeCountQueue.async(flags: .barrier) { [unowned self] in
self._count = newValue
}
}
}
```
-
死鎖問(wèn)題:簡(jiǎn)而言之就是TaskA擁有資源A等待資源B,TaskB 擁有資源B,等待資源A,兩者互相等待,誰(shuí)也拿不到想要的資源。形象點(diǎn)表示,如下圖:
4.png
避免這個(gè)問(wèn)題的方法只有一個(gè):每個(gè)線程按照統(tǒng)一的順序申請(qǐng)資源。比如:TaskA需要資源1和資源2,那么TaskB也按照同樣的順序申請(qǐng)資源1和資源2,就不會(huì)出現(xiàn)這個(gè)問(wèn)題。
-
優(yōu)先級(jí)反轉(zhuǎn)
這種情況表現(xiàn)為“低優(yōu)先級(jí)的Task擁有高優(yōu)先級(jí)需要的資源,導(dǎo)致高優(yōu)先級(jí)不能執(zhí)行”。試想一下,一個(gè)不著急上廁所的人,拿著所有的紙,即使你再著急,你也沒(méi)辦法啊。造成的原因是“一個(gè)低優(yōu)先級(jí)的隊(duì)列的優(yōu)先級(jí)比高優(yōu)先級(jí)隊(duì)列的優(yōu)先級(jí)還高”。還記得創(chuàng)建Queue的時(shí)候有個(gè)優(yōu)先級(jí)的參數(shù)嗎?
DispatchQueue(..., qos: .userInteractive,...),還有分配任務(wù)時(shí)也有一個(gè)優(yōu)先級(jí)參數(shù)queue.async(..., qos: .userInteractive,...)``,如果任務(wù)的優(yōu)先級(jí)高于隊(duì)列的優(yōu)先級(jí),就可能出現(xiàn)這種情況。解決的方法很簡(jiǎn)單:“任務(wù)的優(yōu)先級(jí)不能高于隊(duì)列”
最后
相信GCD在大家的日常開(kāi)發(fā)中經(jīng)常用到,一些常見(jiàn)的用法也早已掌握。文中提到的一些注意點(diǎn)是我個(gè)人曾經(jīng)遇到的坎兒,希望對(duì)大家有所幫助。如果有不對(duì)的地方,歡迎留言指正。不知不覺(jué)已經(jīng)凌晨了,先到這兒,明天寫(xiě)一下Operation。最后悄悄寫(xiě)一句

