9/54
Feb20-Feb26
iOS
Swift
GCD
并發(fā)
譯文
Grand Central Dispatch(GCD)是一種用于管理并發(fā)操作的低級API。 GCD可以提高應(yīng)用程序的響應(yīng)能力冕碟,通過將計算成本高的任務(wù)放到后臺搂根。這是比鎖和線程更容易的并發(fā)模型边锁。
在Swift 3中,GCD進(jìn)行了重大改進(jìn)甜滨,從基于C的API轉(zhuǎn)移到包含新類和新數(shù)據(jù)結(jié)構(gòu)的Swiftier
API拇涤。
第一部分將解釋GCD的功能,并展示幾個基本的GCD functions测柠。第二部分中炼鞠,你將了解一些高級功能。
你將構(gòu)建一個GooglyPuff App轰胁。 GooglyPuff是一個非優(yōu)化的“線程不安全”的應(yīng)用程序谒主,使用Core Image的面部檢測API覆蓋檢測到的面部上的眼鏡。您可以從照片庫中選擇要應(yīng)用此效果的圖像赃阀,或選擇從互聯(lián)網(wǎng)下載的圖像霎肯。
你在本教程中的任務(wù)是使用GCD優(yōu)化應(yīng)用程序,并確保你可以安全地從不同的線程調(diào)用代碼榛斯。
開始
下載初始項目观游,Run。
主屏幕初始為空驮俗。點擊+
懂缕,然后選擇Le Internet
下載預(yù)定義的圖像。點擊第一個圖像王凑,你會看到 googly eyes 添加到臉上提佣。
主要使用四個類:
- PhotoCollectionViewController:Initial view controller, 顯示縮略圖。
-
PhotoDetailViewController:顯示從
PhotoCollectionViewController
選擇的照片荤崇,并添加眼球拌屏。 -
Photo:這是描述照片屬性的
Protocol
。它提供了一個圖像术荤,縮略圖及其相應(yīng)的狀態(tài)倚喂。實現(xiàn)協(xié)議的兩個類:DownloadPhoto
,其從URL的實例實例化照片,以及AssetPhoto
端圈,實例化來自PHAsset
的實例的照片焦读。 -
PhotoManager:這管理所有的
Photo
對象。
這個App有幾個問題舱权。運行應(yīng)用程序時矗晃,下載完成提醒太早了。你會在系列的第二部分解決這個問題宴倍。
在這個第一部分中张症,你將進(jìn)行一些改進(jìn),包括優(yōu)化 googly-fying 進(jìn)程和使PhotoManager
線程安全鸵贬。
GCD概念
要理解GCD俗他,你需要理解并發(fā)和線程相關(guān)的幾個概念勒叠。
并發(fā)
在iOS中佃牛,進(jìn)程或應(yīng)用程序由一個或多個線程組成。線程由操作系統(tǒng)調(diào)度程序獨立管理风响。每個線程可以并發(fā)執(zhí)行嗜浮,但由系統(tǒng)決定是否并發(fā)羡亩、怎樣并發(fā)。
單核設(shè)備可以通過時間分片實現(xiàn)并發(fā)危融。他們將運行一個線程夕春,執(zhí)行上下文切換,然后運行另一個線程专挪。
多核設(shè)備通過并行及志,同時執(zhí)行多個線程。
GCD建立在線程之上寨腔。在底層它管理一個共享線程池速侈。使用GCD,您可以向調(diào)度隊列添加代碼塊或工作項迫卢,由GCD決定執(zhí)行哪些線程倚搬。
在寫代碼時,你發(fā)現(xiàn)一些代碼可以同時運行乾蛤,另外一些不能每界。這樣就允許您使用GCD來并行。
GCD基于系統(tǒng)和可用系統(tǒng)資源決定并行性家卖。需要注意的是眨层,兩個線程并行一定是并發(fā)的,但反之不然上荡。
大致來說趴樱,并發(fā)是關(guān)于結(jié)構(gòu),而并行是關(guān)于執(zhí)行。
隊列
GCD提供由 DispatchQueue 隊列以管理您提交的任務(wù)叁征,并以FIFO順序執(zhí)行它們纳账,以確保提交的第一個任務(wù)是第一個開始的任務(wù)。
DispatchQueue是線程安全的捺疼,這意味著你可以同時從多個線程訪問它們疏虫。當(dāng)你理解調(diào)度隊列如何為代碼的某些部分提供線程安全時,GCD的好處是顯而易見的啤呼。這樣做的關(guān)鍵是選擇正確的DispatchQueue和正確的dispatching功能將您的工作提交到隊列卧秘。
隊列可以是串行或并發(fā)的。串行隊列保證在任何給定時間只運行一個任務(wù)媳友。 GCD控制執(zhí)行時序斯议。你不會知道一個任務(wù)結(jié)束和下一個開始之間的時間产捞。
并發(fā)隊列允許多個任務(wù)同時運行醇锚。保證任務(wù)按照添加的順序啟動。任務(wù)可以以任何順序完成坯临,你不知道下一個任務(wù)要開始的時間焊唬,也不知道在任何時間運行的任務(wù)數(shù)。
參見下面的示例:
注意Task 1看靠,Task 2和Task 3一個接一個地快速啟動赶促。而任務(wù)1 等了一段時間在任務(wù)0之后開始。另外挟炬,雖然任務(wù)3在任務(wù)2之后開始鸥滨,但它更快完成。
何時開始任務(wù)的決定完全由GCD決定谤祖。如果一個任務(wù)的執(zhí)行時間與另一個任務(wù)重疊婿滓,那么由GCD決定是否應(yīng)該在不同的核上運行(如果一個任務(wù)可用),或者改為執(zhí)行上下文切換以運行不同的任務(wù)粥喜。
GCD提供三種主要類型隊列:
- 主隊列 ( Main Queue) :在主線程上運行凸主,是一個串行隊列。
- 全局隊列 ( Global Queues) :由整個系統(tǒng)共享的并發(fā)隊列额湘。有四個這樣的隊列具有不同的優(yōu)先級:高卿吐,默認(rèn),低和后臺锋华。后臺是I/O限制嗡官。
- **自定義隊列 ( Custom queues) **:創(chuàng)建的隊列,可以是串行或并發(fā)的毯焕。這些實際上會被一個全局隊列處理谨湘。
當(dāng)設(shè)置全局并發(fā)隊列時,不直接指定優(yōu)先級。而是指定Quality of Serive(QoS)類屬性紧阔。這將指示任務(wù)的重要性坊罢,并指導(dǎo)GCD確定給予任務(wù)的優(yōu)先級。
QoS類別:
- 用戶交互式:這表示需要立即完成以提供良好的用戶體驗的任務(wù)擅耽。用于UI更新活孩,事件處理和需要低延遲的小型工作負(fù)載。在執(zhí)行應(yīng)用程序期間乖仇,在此類中完成的工作總量應(yīng)該很小憾儒。這應(yīng)該在主線程上運行。
- 用戶啟動:表示從UI啟動并可以異步執(zhí)行的任務(wù)乃沙。它應(yīng)該在用戶正在等待即時結(jié)果時使用起趾,并且用于需要繼續(xù)用戶交互的任務(wù)。這將被映射到高優(yōu)先級全局隊列警儒。
- 實用程序:這表示長時間運行的任務(wù)训裆,通常是進(jìn)度指示條。用于計算蜀铲,I / O边琉,網(wǎng)絡(luò),連續(xù)數(shù)據(jù)和類似任務(wù)记劝。這將被映射到低優(yōu)先級全局隊列变姨。
- 后臺:這表示用戶不直接感知的任務(wù)。用于提取厌丑,維護(hù)和其他不需要用戶交互并且不對時間敏感的任務(wù)定欧。這將被映射到后臺優(yōu)先級全局隊列。
同步和異步
同GCD怒竿,你可以同步或異步分派任務(wù)砍鸠。
同步函數(shù)在任務(wù)完成后將控制返回給調(diào)用者。
異步函數(shù)立即返回愧口,程序并不等待它執(zhí)行完成睦番。異步函數(shù)不會阻止當(dāng)前執(zhí)行線程,而會繼續(xù)執(zhí)行下一個函數(shù)耍属。
管理任務(wù)
現(xiàn)在你已經(jīng)聽說過任務(wù)了托嚣。為了本教程的目的,您可以將任務(wù)視為閉包 (closure)厚骗。閉包是自包含的示启,可調(diào)用的代碼塊,可以存儲和傳遞领舰。(在Objective-C中叫做 Block)
提交到DispatchQueue的任務(wù)由DispatchWorkItem封裝夫嗓。你可以配置一個DispatchWorkItem的行為迟螺,如它的QoS類或是否產(chǎn)生一個新的分離的線程。
處理后臺任務(wù)
來改進(jìn)App吧舍咖!
添加一些照片從您的照片庫或使用Le Internet
選項下載幾張矩父。點擊照片。注意照片詳細(xì)視圖顯示需要多長時間排霉。當(dāng)在較慢的設(shè)備上查看大圖像時窍株,滯后會更明顯。
在viewDidLoad()
中太多的工作很容易導(dǎo)致view出現(xiàn)前長時間等待攻柠。如果不是必要的加載球订,可以把它放在后臺執(zhí)行。
這就是一個DispatchQueueAsync 操作瑰钮。
打開PhotoDetailViewController.swift
冒滩。修改viewDidLoad()
替換這兩行:
let overlayImage = faceOverlayImageFromImage(image)
fadeInNewImage(overlayImage)
替換代碼:
DispatchQueue.global(qos:.userInitiated).async { // 1
let overlayImage = self.faceOverlayImageFromImage(self.image)
DispatchQueue.main.async { // 2
self.fadeInNewImage(overlayImage) // 3
}
}
解釋:
- 將工作移到后臺全局隊列,并以異步方式在閉包中運行工作浪谴。這讓
viewDidLoad()
在主線程完成开睡,并使加載感覺更清晰。同時较店,面部檢測處理開始并且將在稍后的時間完成士八。 - 面部檢測處理完成容燕,已生成新圖像。因為你想使用這個新的圖像來更新你的UIImageView蘸秘,你設(shè)置image在主隊列官卡。你必須總是在主線程上訪問UIKit類!
- 最后醋虏,用
fadeInNewImage(_:)
更新UI寻咒,執(zhí)行新的 googly 眼睛圖像的淡入轉(zhuǎn)換。
運行應(yīng)用程序颈嚼。通過Le Internet
選項下載照片毛秘。選擇一個照片,你會注意到視圖控制器加載明顯更快阻课,并添加了googly眼睛一個短暫的延遲:
現(xiàn)在叫挟,即使你試圖加載一個巨大的圖像,你的應(yīng)用程序也不會卡頓限煞。
一般來說抹恳。你用async
, 當(dāng)你需要執(zhí)行基于網(wǎng)絡(luò)或CPU密集型任務(wù)在后臺署驻,而不是阻塞當(dāng)前線程奋献。
這里是如何使用各種隊列與async
:
- 主隊列:這是一個常見的選擇健霹,以在并行隊列中的任務(wù)完成工作后更新UI。調(diào)用async瓶蚂,保證這個新任務(wù)將在當(dāng)前方法結(jié)束后的某個時間在主隊列執(zhí)行糖埋。
- 全局隊列:這是在后臺執(zhí)行非UI工作的常見選擇。
- 自定義串行隊列:當(dāng)你想要連續(xù)執(zhí)行后臺工作并跟蹤它窃这。這消除了資源競爭阶捆,因為您知道一次只有一個任務(wù)正在執(zhí)行。請注意钦听,如果您需要來自方法的數(shù)據(jù)洒试,則必須內(nèi)聯(lián)另一個閉包以檢索它或者考慮使用sync。
延遲任務(wù)執(zhí)行
DispatchQueue
允許你延遲任務(wù)執(zhí)行朴上。注意不要使用這個來解決競爭條件或其他時序的bug垒棋,例如引入延遲。當(dāng)您希望任務(wù)在特定時間運行時使用此選項痪宰。
對你的應(yīng)用程序的用戶體驗來說叼架。用戶可能會對他們第一次打開應(yīng)用程序時做什么感到困惑是嗎?
如果沒有任何照片衣撬,最好向用戶顯示提示乖订。你還應(yīng)該考慮用戶如何使用新App。如果你顯示一個提示太快具练,他們可能會錯過它乍构,因為他們的眼睛徘徊在視圖的其他部分。顯示提示之有一秒鐘的延遲足以吸引用戶的注意力并指導(dǎo)他們扛点。
打開PhotoCollectionViewController.swift
并在showOrHideNavPrompt()
的實現(xiàn)以下代碼:
let delayInSeconds = 1.0 // 1
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { // 2
let count = PhotoManager.sharedManager.photos.count
if count > 0 {
self.navigationItem.prompt = nil
} else {
self.navigationItem.prompt = "Add photos with faces to Googlyify them!"
}
}
解釋:
- 為延遲時間量指定一個變量哥遮。
- 等待指定的時間,然后異步運行更新照片計數(shù)陵究,并更新prompt眠饮。
Build & Run。在顯示提示之前應(yīng)該有一點延遲:
想知道什么時候適合使用asyncAfter
铜邮?一般來說仪召,在主隊列中使用它是一個不錯的選擇。在其他隊列(如全局后臺隊列或自定義串行隊列)上使用asyncAfter時松蒜,您需要小心扔茅。
為什么不使用Timer?你可以考慮使用它牍鞠,如果你有重復(fù)的任務(wù)咖摹。這里有兩個原因用asyncAfter
。
- 一個是可讀性难述。要使用Timer萤晴,您必須定義一個方法吐句,然后使用選擇器或調(diào)用定義的方法創(chuàng)建計時器。使用DispatchQueue和async只需添加一個閉包店读。
- Timer 被安排在Run Loop上嗦枢,所以你還必須確保它被安排在正確的Run Loop上。在這方面屯断,使用調(diào)度隊列更容易文虏。
管理 Singletons
Singletons,在iOS中非常流行殖演。
Singletons 的一個常見問題是氧秘,它們通常不是線程安全的。因為它們通常被多個Controller 同時訪問趴久。PhotoManager
是一個單例丸相,所以你需要考慮這個問題。
線程安全的代碼可以安全地從多個線程或并發(fā)任務(wù)調(diào)用彼棍,而不會導(dǎo)致任何問題灭忠。非線程安全的代碼只能一次在一個上下文中運行。
在單例實例的初始化期間以及在對實例的讀取和寫入期間座硕,需要考慮兩種線程安全性情況弛作。
初始化是容易的情況,這是由于Swift初始化全局變量的方式华匾。全局變量在首次訪問時被初始化映琳,并且它們被保證以原子方式初始化。也就是說瘦真,執(zhí)行初始化的代碼被視為原子操作刊头,保證在任何其他線程訪問全局變量之前完成黍瞧。
打開PhotoManager.swift
查看如何初始化單例:
private let _sharedManager = PhotoManager()
私有全局 _sharedManager
變量用于惰性初始化 PhotoManager
诸尽。 這僅發(fā)生在第一次訪問:_
class var sharedManager: PhotoManager {
return _sharedManager
}
公共函數(shù)sharedManager
返回私有_sharedManager
變量。 Swift確保此操作是線程安全的_
在訪問操作共享內(nèi)部數(shù)據(jù)的單例中的代碼時印颤,仍然需要處理線程安全您机。可以通過同步數(shù)據(jù)訪問等方法來處理此問題年局。在下一節(jié)中际看,您將看到一種方法。
處理 Readers-Writers 問題
在Swift中矢否,用let
關(guān)鍵字聲明的任何變量都被認(rèn)為是一個常量仲闽,并且是只讀的和線程安全的。然而僵朗,使用var關(guān)鍵字聲明變量赖欣,它是可變的屑彻,并且不是線程安全的,除非數(shù)據(jù)類型被設(shè)計為這樣顶吮。 Swift集合類型(如Array
和Dictionary
)在聲明為可變時不是線程安全的社牲。
雖然許多線程可以同時讀取Array
的可變實例,這沒有問題悴了,但是讓一個線程讀搏恤,讓一個線程修改數(shù)組是不安全的。你的單例不能防止這種情況發(fā)生湃交。
讓我們看下 addPhoto(_:)
在PhotoManager.swift
func addPhoto(_ photo: Photo) {
_photos.append(photo)
DispatchQueue.main.async {
self.postContentAddedNotification()
}
}
這是一個寫方法熟空,因為它修改了一個可變數(shù)組對象。
現(xiàn)在來看看photos
:
fileprivate var _photos: [Photo] = [ ]
var photos: [Photo] {
return _photos
}
此屬性的getter被稱為讀取方法搞莺,因為它正在讀取mutable數(shù)組痛阻。 調(diào)用者獲得數(shù)組的副本,并且防止不適當(dāng)?shù)馗淖冊紨?shù)組腮敌。 這不提供任何保護(hù)阱当,以防止一個線程調(diào)用addPhoto(_:)
,而另一個線程調(diào)用getter的photos
屬性糜工。_
注意:在上面的代碼中弊添,為什么調(diào)用者得到的照片數(shù)組的副本? 在Swift中捌木,參數(shù)和返回類型的函數(shù)通過引用或值傳遞油坝。
按值傳遞將導(dǎo)致對象的副本,對副本的更改不會影響原始副本刨裆。 默認(rèn)情況下澈圈,在Swift類中,實例通過引用傳遞帆啃,而struct通過值傳遞瞬女。 Swift的內(nèi)置數(shù)據(jù)類型,如數(shù)組和字典努潘,被實現(xiàn)為結(jié)構(gòu)體诽偷。
看起來當(dāng)你來回傳遞集合時,代碼中有很多復(fù)制疯坤。 不要擔(dān)心這種內(nèi)存使用的影響报慕。 Swift集合類型被優(yōu)化以僅在必要時進(jìn)行復(fù)制,例如當(dāng)通過值的數(shù)組在傳遞之后第一次被修改時压怠。
這是經(jīng)典的Readers-Writers
問題眠冈。 GCD提供了一個優(yōu)雅解決方案創(chuàng)建讀寫鎖,通過使用 dispatch barriers
菌瘫。dispatch barriers
是在使用并發(fā)隊列時作為串行式瓶頸的一組函數(shù)蜗顽。
當(dāng)您向調(diào)度隊列提交DispatchWorkItem
時玄柠,您可以設(shè)置標(biāo)志以指示它應(yīng)該是在特定時間在該指定隊列上執(zhí)行的唯一項目。 這意味著提交到隊列之前的所有項目必須在DispatchWorkItem
執(zhí)行之前完成诫舅。
當(dāng)輪到DispatchWorkItem
時羽利,GCD確保隊列在那段時間內(nèi)不執(zhí)行任何其他任務(wù)。 一旦完成刊懈,隊列返回到其默認(rèn)狀態(tài)这弧。
下圖說明了barriers
對各種異步任務(wù)的影響:
注意在正常操作中隊列的行為就像一個普通的并發(fā)隊列。 但是當(dāng)barriers
執(zhí)行時虚汛,它本質(zhì)上像一個串行隊列匾浪。 也就是說,barriers
是唯一執(zhí)行的任務(wù)卷哩。 在barriers
完成后蛋辈,隊列返回到正常的并發(fā)隊列。
在全局后臺并行隊列中使用barriers
時請謹(jǐn)慎将谊,因為這些隊列是共享資源冷溶。 在自定義串行隊列中使用障礙是多余的,因為它已經(jīng)連續(xù)執(zhí)行尊浓。 在自定義并發(fā)隊列中使用barriers
是最好的選擇逞频。
你將使用自定義并發(fā)隊列來處理您的barrier
功能并隔離讀取和寫入功能。 并發(fā)隊列將允許同時進(jìn)行多個讀取操作栋齿。
打開PhotoManager.swift
并在_photos
聲明之上添加一個私有屬性:
fileprivate let concurrentPhotoQueue =
DispatchQueue(
label: "com.raywenderlich.GooglyPuff.photoQueue", // 1
attributes: .concurrent) // 2
這會將concurrentPhotoQueue
初始化為并發(fā)隊列苗胀。
- 您在調(diào)試期間使用描述性名稱設(shè)置標(biāo)簽,這是有幫助的瓦堵。 通常基协,您將使用顛倒的DNS樣式命名約定。
- 指定并發(fā)隊列菇用。
接下來澜驮,使用以下代碼替換addPhoto(_:)
func addPhoto(_ photo: Photo) {
concurrentPhotoQueue.async(flags: .barrier) { // 1
self._photos.append(photo) // 2
DispatchQueue.main.async { // 3
self.postContentAddedNotification()
}
}
}
這里是工作原理:
-
barrier
異步分派寫操作。當(dāng)它執(zhí)行時刨疼,它將是隊列中的唯一項目泉唁。 - 將對象添加到數(shù)組。
- 最后揩慕,您發(fā)布了您添加了照片的通知。這個通知應(yīng)該發(fā)布在主線程上扮休,因為它會做UI工作迎卤。因此,您將另一個任務(wù)異步分派到主隊列以觸發(fā)通知玷坠。
還需要實現(xiàn)照片讀取方法蜗搔。
為了確保線程安全劲藐,您需要在concurrentPhotoQueue
隊列上執(zhí)行讀取。您需要從函數(shù)調(diào)用返回數(shù)據(jù)樟凄,因此異步調(diào)度不會打斷它聘芜。在這種情況下,syc
將是一個很好的候選缝龄。
使用sync
來跟蹤您的工作與調(diào)度障礙汰现,或當(dāng)您需要等待操作完成,然后才能使用由閉包處理的數(shù)據(jù)叔壤。
你需要小心瞎饲。如果你調(diào)用sync
并且當(dāng)前隊列已經(jīng)運行。這將導(dǎo)致死鎖情況炼绘。
兩個(或有時更多)項目 - 在大多數(shù)情況下嗅战,線程 - 被稱為死鎖,如果他們都卡住等待對方完成或執(zhí)行另一個操作俺亮。第一個不能完成驮捍,因為它在等待第二個完成。但第二個不能完成脚曾,因為它在等待第一個完成厌漂。
以下是使用同步功能的時間和位置:
- 主隊列:非常小心,這種情況也有潛在的死鎖狀態(tài)斟珊。
- 全局隊列:這是一個很好的候選苇倡,通過調(diào)度障礙或等待任務(wù)完成同步工作,所以你可以執(zhí)行進(jìn)一步處理囤踩。
-
自定義序列隊列:非常小心旨椒,如果您在隊列中運行并調(diào)用
sync
鎖定同一個隊列,那么肯定會導(dǎo)致死鎖堵漱。
仍然在PhotoManager.swift
中修改photos
屬性getter:
var photos: [Photo] {
var photosCopy: [Photo]!
concurrentPhotoQueue.sync { // 1
photosCopy = self._photos // 2
}
return photosCopy
}
以下是逐步進(jìn)行的操作:
-
sync
分派到concurrentPhotoQueue
上以執(zhí)行讀取综慎。 - 將照片陣列的副本存儲在
photosCopy
中并返回。
構(gòu)建并運行應(yīng)用程序勤庐。 通過Le Internet
選項下載照片示惊。 它應(yīng)該像以前一樣,但在底下愉镰,你有一些健康的線程米罚。
恭喜 - PhotoManager
Singleton 現(xiàn)在是線程安全的。無論在哪里或如何讀或?qū)懭胝掌商剑愣伎梢韵嘈潘鼤园踩姆绞酵瓿伞?/p>
下一步
在這個Grand Central Dispatch
教程中录择,您學(xué)習(xí)了如何使代碼線程安全审丘,以及如何在執(zhí)行CPU密集型任務(wù)時保持主線程的響應(yīng)员帮。
您可以下載已完成的項目,其中包含所有改進(jìn)。在本教程的第二部分中昌妹,你將繼續(xù)改進(jìn)此項目议忽。
如果您打算優(yōu)化自己的應(yīng)用程序恃鞋,您應(yīng)該使用Instruments中的Time Profile
文件模板來分析您的工作洞慎。
你也許還想看看Rob Pike對并發(fā)與并行性的這個精彩演講。
我們的iOS并發(fā)與GCD和操作視頻教程系列也涵蓋了很多我們在本教程中涵蓋的相同的主題菱皆。
在本教程的下一部分须误,您將深入了解GCD的API,以做更酷的東西搔预。