并發(fā)編程:API 及挑戰(zhàn)
線程
線程(thread)是組成進程的子單元,操作系統(tǒng)的調(diào)度器可以對線程進行單獨的調(diào)度。實際上级野,所有的并發(fā)編程 API 都是構(gòu)建于線程之上的 —— 包括 GCD 和操作隊列(operation queues)蓖柔。
多線程可以在單核 CPU 上同時(或者至少看作同時)運行。操作系統(tǒng)將小的時間片分配給每一個線程矛双,這樣就能夠讓用戶感覺到有多個任務(wù)在同時進行。如果 CPU 是多核的蟆豫,那么線程就可以真正的以并發(fā)方式被執(zhí)行议忽,從而減少了完成某項操作所需要的總時間。 --? 記得有張圖很好J酢U恍摇!Grand Central Dispatch 基礎(chǔ)教程:Part 1/2
需要重點關(guān)注的是帮辟,你無法控制你的代碼在什么地方以及什么時候被調(diào)度,以及無法控制執(zhí)行多長時間后將被暫停,以便輪換執(zhí)行別的任務(wù)。這種線程調(diào)度是非常強大的一種技術(shù)。
(1)pthread太復(fù)雜
(2)NSThread?是 Objective-C 對 pthread 的一個封裝柳弄。通過封裝萍丐,在 Cocoa 環(huán)境中壳影,可以讓代碼看起來更加親切掺栅。例如搏明,開發(fā)者可以利用 NSThread 的一個子類來定義一個線程由桌,在這個子類的中封裝需要在后臺線程運行的代碼娃循。- start捞蚂,- isFinished
兩個基于隊列的并發(fā)編程 API :GCD 和 operation queue 。它們集中管理一個被大家協(xié)同使用的線程池。
(3)GCD
通過 GCD嘉抓,開發(fā)者不用再直接跟線程打交道了,只需要向隊列中添加代碼塊即可,GCD 在后端管理著一個線程池舌剂。GCD 不僅決定著你的代碼塊將在哪個線程被執(zhí)行,它還根據(jù)可用的系統(tǒng)資源對這些線程進行管理均驶。這樣可以將開發(fā)者從線程管理的工作中解放出來跑筝,通過集中的管理線程世剖,來緩解大量線程被創(chuàng)建的問題遭庶。
GCD 帶來的另一個重要改變是,作為開發(fā)者可以將工作考慮為一個隊列瓜富,而不是一堆線程,這種并行的抽象模型更容易掌握和使用。
GCD 公開有 5 個不同的隊列:<1> 運行在主線程中的 main queue,<2> 3 個不同優(yōu)先級的后臺隊列,以及<3> 一個優(yōu)先級更低的后臺隊列(用于 I/O)啡邑。
可以創(chuàng)建自定義隊列:串行或者并行隊列果漾。在自定義隊列中被調(diào)度的所有 block 最終都將被放入到系統(tǒng)的全局隊列中和線程池中焚鹊。
(4)Operation Queues
操作隊列(operation queue)是由 GCD 提供的一個隊列模型的 Cocoa 抽象申屹。GCD 提供了更加底層的控制杆煞,而操作隊列則在 GCD 之上實現(xiàn)了一些方便的功能贫悄,這些功能對于 app 的開發(fā)者來說通常是最好最安全的選擇盏阶。
NSOperationQueue有兩種不同類型的隊列:主隊列和自定義隊列。主隊列運行在主線程之上乌逐,而自定義隊列在后臺執(zhí)行。在兩種類型中浙踢,這些隊列所處理的任務(wù)都使用NSOperation的子類來表述绢慢。
你可以通過重寫main或者start(擁有更多的控制權(quán),在操作中可以執(zhí)行異步任務(wù) - isExecuting洛波,isFinished胰舆,isCancelled)方法 來定義自己的operations。
為了讓操作隊列能夠捕獲到操作的改變蹬挤,需要將狀態(tài)的屬性以配合 KVO 的方式進行實現(xiàn)缚窿。
addOperation: -- addOperationWithBlock:
比 GCD完善的功能:= 最大并發(fā) + 隊列優(yōu)先級 + 依賴關(guān)系
(1)maxConcurrentOperationCount屬性來控制一個特定隊列中可以有多少個操作參與并發(fā)執(zhí)行。將其設(shè)置為 1 的話焰扳,你將得到一個串行隊列倦零,這在以隔離為目的的時候會很有用。
(2)根據(jù)隊列中operation的優(yōu)先級對其進行排序吨悍,這不同于 GCD 的隊列優(yōu)先級扫茅,它只影響當(dāng)前隊列中所有被調(diào)度的 operation 的執(zhí)行先后。超越5個標(biāo)準(zhǔn)優(yōu)先級之外的op執(zhí)行順序控制育瓜,可以在op之間指定依賴關(guān)系:
[intermediateOperation addDependency:operation1];
操作隊列的性能比 GCD 要低那么一點葫隙,操作隊列是并發(fā)編程的首選工具。
Run Loops
在主 dispatch/operation 隊列中躏仇, run loop 將直接配合任務(wù)的執(zhí)行恋脚,它提供了一種異步執(zhí)行代碼的機制。
一個 run loop 總是綁定到某個特定的線程中焰手。main run loop 是與主線程相關(guān)的慧起,在每一個 Cocoa 和 CocoaTouch 程序中,這個 main run loop 都扮演了一個核心角色册倒,它負(fù)責(zé)處理 UI 事件蚓挤、計時器,以及其它內(nèi)核相關(guān)事件。無論你什么時候設(shè)置計時器灿意、使用NSURLConnection或者調(diào)用performSelector:withObject:afterDelay:估灿,其實背后都是 run loop 在處理這些異步任務(wù)。
無論何時你使用 run loop 來執(zhí)行一個方法的時候缤剧,都需要記住一點:run loop 可以運行在不同的模式中馅袁,每種模式都定義了一組事件,供 run loop 做出響應(yīng)荒辕。這在對應(yīng) main run loop 中暫時性的將某個任務(wù)優(yōu)先執(zhí)行這種任務(wù)上是一種聰明的做法汗销。
滾動,trackingMode抵窒,不會響應(yīng)defaultMode添加的計時器弛针,停止?jié)L動,回到default才響應(yīng)李皇。如果滾動時候要響應(yīng)計時器削茁,需要將其設(shè)為NSRunLoopCommonModes的模式,并添加到 run loop 中掉房。
如果你真需要在別的線程中添加一個 run loop 茧跋,那么不要忘記在 run loop 中至少添加一個 input source 。如果 run loop 中沒有設(shè)置好的 input source卓囚,那么每次運行這個 run loop 瘾杭,它都會立即退出。
并發(fā)編程中面臨的挑戰(zhàn)
資源共享
并發(fā)編程中許多問題的根源就是在多線程中訪問共享資源哪亿。在多線程中任何一個共享的資源都可能是一個潛在的沖突點富寿,你必須精心設(shè)計以防止這種沖突的發(fā)生。
在多線程里面訪問一個共享的資源锣夹,如果沒有一種機制來確保在線程 A 結(jié)束訪問一個共享資源之前页徐,線程 B 就不會開始訪問該共享資源的話,資源競爭的問題就總是會發(fā)生银萍。多線程需要一種互斥的機制來訪問共享資源变勇、
互斥鎖? -- ?解決了競態(tài)條件的問題
(1)互斥訪問的意思就是同一時刻,只允許一個線程訪問某個特定資源贴唇。為了保證這一點搀绣,每個希望訪問共享資源的線程,首先需要獲得一個共享資源的互斥鎖戳气,一旦某個線程對資源完成了操作链患,就釋放掉這個互斥鎖,這樣別的線程就有機會訪問該共享資源了瓶您。
(2)為了解決由 CPU 的優(yōu)化策略引起的副作用麻捻,還需要引入內(nèi)存屏障纲仍。通過設(shè)置內(nèi)存屏障,來確保沒有無序執(zhí)行的指令能跨過屏障而執(zhí)行贸毕。
將一個屬性聲明為 atomic 表示每次訪問該屬性都會進行隱式的加鎖和解鎖操作郑叠。
死鎖 ?-- ?自己持有一個鎖,想要拿對象的鎖
當(dāng)多個線程在相互等待著對方的結(jié)束時明棍,就會發(fā)生死鎖乡革。
資源饑餓(Starvation)-- 沒有寫入鎖可持有讀取鎖,持有讀取鎖等待寫入鎖摊腋,造成其他饑餓
鎖定的共享資源會引起讀寫問題沸版。大多數(shù)情況下,限制資源一次只能有一個線程進行讀取訪問其實是非常浪費的兴蒸。因此视粮,在資源上沒有寫入鎖的時候,持有一個讀取鎖是被允許的类咧。這種情況下馒铃,如果一個持有讀取鎖的線程在等待獲取寫入鎖的時候蟹腾,其他希望讀取資源的線程則因為無法獲得這個讀取鎖而導(dǎo)致資源饑餓的發(fā)生痕惋。
優(yōu)先級反轉(zhuǎn)(亂入,第三者) ?--? 程序在運行時低優(yōu)先級的任務(wù)阻塞了高優(yōu)先級的任務(wù)娃殖,有效的反轉(zhuǎn)了任務(wù)的優(yōu)先級值戳。
高低優(yōu)先級任務(wù)共享資源,低優(yōu)先級任務(wù)拿到鎖炉爆,高優(yōu)先級任務(wù)阻塞堕虹,出現(xiàn)中優(yōu)先級任務(wù),因為高的被阻塞芬首,所以中的優(yōu)先級最高赴捞,阻塞低的,然后自己執(zhí)行郁稍,這個過程中間接阻塞了高優(yōu)先級任務(wù)赦政。簡直就是亂入帶來的后果。
總結(jié)
在開發(fā)中耀怜,關(guān)鍵的一點就是盡量讓并發(fā)模型保持簡單恢着,這樣可以限制所需要的鎖的數(shù)量。
我們建議采納的安全模式是這樣的:從主線程中提取出要使用到的數(shù)據(jù)财破,并利用一個操作隊列在后臺處理相關(guān)的數(shù)據(jù)掰派,最后回到主隊列中來發(fā)送你在后臺隊列中得到的結(jié)果。使用這種方式左痢,你不需要自己做任何鎖操作靡羡,這也就大大減少了犯錯誤的幾率系洛。
常見的后臺實踐
如何并發(fā)地使用 Core Data ,如何并行繪制 UI 亿眠,如何做異步網(wǎng)絡(luò)請求等碎罚。如何異步處理大型文件,以保持較低的內(nèi)存占用纳像。
操作隊列 (Operation Queues) 還是 GCD ?
其中 GCD 是基于 C 的底層的 API 荆烈,而操作隊列則是 GCD 實現(xiàn)的 Objective-C API。
OP 比 GCD 最重要的一個就是可以取消在任務(wù)處理隊列中的任務(wù)竟趾。相反憔购,GCD 給予你更多的控制權(quán)力以及操作隊列中所不能使用的底層函數(shù)。
后臺的 Core Data?
-- Core Data Programming - Part V: Advanced Topics - Concurrency
絕對不要在線程間傳遞 managed objects等岔帽。要想傳遞這樣的對象玫鸟,正確做法是通過傳遞它的 object ID ,然后從其他對應(yīng)線程所綁定的 context 中去獲取這個對象犀勒。
然后屎飘,如果你想要做大量的處理,那么把它放到一個后臺上下文來做會比較好贾费。一個典型的應(yīng)用場景是將大量數(shù)據(jù)導(dǎo)入到 Core Data 中钦购。
(1)我們?yōu)閷?dǎo)入工作單獨創(chuàng)建一個操作
(2)我們創(chuàng)建一個 managed object context ,它和主 managed object context 使用同樣的 persistent store coordinator
(3)一旦導(dǎo)入 context 保存了褂萧,我們就通知 主 managed object context 并且合并這些改變
源碼:
我們創(chuàng)建一個NSOperation的子類押桃,將其叫做ImportOperation,我們通過重寫main方法导犹,用來處理所有的導(dǎo)入工作唱凯。這里我們使用NSPrivateQueueConcurrencyType(也就還有一種MainQueue的)來創(chuàng)建一個獨立并擁有自己的私有 dispatch queue 的 managed object context,這個 context 需要管理自己的隊列谎痢。在隊列中的所有操作必須使用performBlock或者performBlockAndWait來進行觸發(fā)磕昼。這點對于保證這些操作能在正確的線程上執(zhí)行是相當(dāng)重要的。
NSFetchedResultsCon做數(shù)據(jù)源 + Store用于配置各種context + operation
Store: ?-- ?暴露一個objContext节猿,-save方法票从,-privateContext
<1> 初始化注冊通知DidSave = 如果是privateContext發(fā)送的通知,非privateContext執(zhí)行合并改變 (現(xiàn)在在后臺 context中導(dǎo)入的數(shù)據(jù)還不能傳送到主 context 中沐批,顯式地讓它這么去做纫骑。)
<2> getter,暴露非Private?objContext
<3> getter九孩,Private objContext
<4> getter先馆,pSC(<- objectModel) => addPersis:configure:URL(storeURL) = store
<5> getter,objectModel(modelURL)
ImportViewController
<1> start(配置operation躺彬,添加隊列)(前臺更新UI煤墙,后臺處理數(shù)據(jù)導(dǎo)入) + cancel + progress + tableView
<2> init + viewDidLoad()
ImportOperation ?-- ?privateContext
progressCallback ?-- ?需要注意的是梅惯,更新進度條必須在主線程中完成,否則會導(dǎo)致 UIKit 崩潰仿野。?
批量保存铣减。在導(dǎo)入較大的數(shù)據(jù)時,我們需要定期保存脚作,逐漸導(dǎo)入葫哗,否則內(nèi)存很可能就會被耗光,性能一般也會更壞球涛。每 250 次導(dǎo)入就保存一次劣针。
<1> init
<2> main ?-- ?import?
FetchedResultsTableDataSource? --? fetchedResultsController(mainContext)是數(shù)據(jù)源 like NSArray
<1> tableView + fetchedResultsController
<2> - init + changePredicate + selectedItem + configureCell
<3> DataSource4:titleForHeader
<4> Delegate4:通過fetchedResultsController這個數(shù)據(jù)源數(shù)據(jù)的動態(tài)改變來更新tableView
Stop
<1> Model
<2> Category2:fetch + insert
PS:
<1> performBlockAndWait 可以通過隊列cancel掉,而performBlock不可以
<2> cell不動態(tài)加入亿扁,發(fā)生在最不可能的類中捺典。_mainManagedObjectContext getter方法中錯誤
其他
(1)導(dǎo)入操作時,我們將整個文件都讀入到一個字符串中从祝,然后將其分割成行 =>?相對小的文件襟己。對于大文件,最好采用惰性讀取 (lazily read) 的方式逐行讀入牍陌。使用輸入流的方式來實現(xiàn)這個特性擎浴。
(2)在 app 第一次運行時,除大量數(shù)據(jù)導(dǎo)入 Core Data?以外呐赡,<1> 也可以在你的 app bundle 中直接放一個 sqlite 文件退客。<2>從一個可以動態(tài)生成數(shù)據(jù)的服務(wù)器下載骏融。這些方式可以節(jié)省不少在設(shè)備上的處理時間链嘀。
(3)對于 child contexts 爭議。不要在后臺操作中使用它档玻。<1> 如果你以主 context 的 child 的方式創(chuàng)建了一個后臺 context 的話怀泊,保存這個后臺 context 將阻塞主線程。<2> 將主 context 作為后臺 context 的 child 的話误趴,實際上和與創(chuàng)建兩個傳統(tǒng)的獨立 contexts 來說是沒有區(qū)別的霹琼。因為你仍然需要手動將后臺的改變合并回主 context 中去。
(4)設(shè)置一個 persistent store coordinator 和?兩個獨立的 contexts 是在后臺處理 Core Data BP 凉当。
后臺 UI 代碼
為了避免在運行 block 時訪問到已被釋放的對象枣申,在 block 中我們又需要將其轉(zhuǎn)回 strong 引用。
后臺繪制 ?-- ?之前 + 如何做 + 操作隊列放入取消 + CALayer異步繪制
確定drawRect:是你的應(yīng)用的性能瓶頸看杭,那么你可以將這些繪制代碼放到后臺去做忠藤。做之前,檢查下看看是不是有其他方法來楼雹。<1> 考慮使用 CALayers 或者預(yù)先渲染圖片而不去做 CG 繪制模孩。<2> Florian 對在真機上圖像性能測量的帖子?<3> UIKit 工程師 Andy Matuschak 對個各種方式的權(quán)衡的評論尖阔。
如何做:在后臺繪制代碼會是你的最好選擇時再這么做。把drawRect:中的代碼放到一個后臺操作中去做就可以了榨咐。然后將原本打算繪制的視圖用一個 imageView 來替換介却,等到操作執(zhí)行完后再去更新。在繪制的方法中块茁,使用UIGraphicsBeginImageContextWithOptions(-,-,0_scale自動傳入) + get Image + End
tableView OR collectionView 的 cell 上做了自定義繪制的話齿坷,最好放入 operation 的子類中去。你可以將它們添加到后臺操作隊列数焊,也可以在用戶將 cell 滾動出邊界時的didEndDisplayingCell委托方法中進行取消胃夏。這些技巧都在 2012 年的WWDCSession 211 -- Building Concurrent User Interfaces on iOS
CALayer? --? drawsAsynchronously
異步網(wǎng)絡(luò)請求處理
你的所有網(wǎng)絡(luò)請求都應(yīng)該采取異步的方式完成。最好還是不要阻塞線程昌跌。(1)使用NSURLSession/Connection的異步方法仰禀,并且把所有操作轉(zhuǎn)化為 operation 來執(zhí)行。operationQueue的強大功能:控制并發(fā)操作的數(shù)量蚕愤,添加依賴答恶,以及取消操作。
NSURLConnection是通過 run loop 來發(fā)送事件的萍诱。因為時間發(fā)送不會花多少時間悬嗓,因此最簡單的是只使用 main run loop?。用后臺線程來處理輸入的數(shù)據(jù)了裕坊。
(2)另一種方式是AFNetworking:建立一個獨立的線程包竹,為建立的線程設(shè)置自己的 run loop,然后在其中調(diào)度 URL 連接籍凝。不推薦
自定義的 operation 子類中的start方法:+ cancel: + isFinished,isExecuting + 代理回調(diào)
進階:后臺文件 I/O
大文件對內(nèi)存負(fù)擔(dān)大周瞎,要解決這個問題,構(gòu)建一個類饵蒂,負(fù)責(zé)一行一行讀取文件而不是一次將整個文件讀入內(nèi)存声诸,另外要在后臺隊列處理文件,以保持應(yīng)用響應(yīng)用戶的操作退盯。
異步處理文件的NSInputStream? --? 官方文檔
不管你是否使用 streams彼乌,大體上逐行讀取一個文件的模式是這樣的:
(1)建立一個中間緩沖層以提供,當(dāng)沒有找到換行符號的時候可以向其中添加數(shù)據(jù)
(2)從 stream 中讀取一塊數(shù)據(jù)
(3)對于這塊數(shù)據(jù)中發(fā)現(xiàn)的每一個換行符渊迁,取中間緩沖層慰照,向其中添加數(shù)據(jù),直到(并包括)這個換行符琉朽,并將其輸出
(4)將剩余的字節(jié)添加到中間緩沖層去
(5)回到 2毒租,直到 stream 關(guān)閉
源碼:
絕大部分時候,使用逐塊讀入的方式來處理大文件漓骚,是非常有用的技術(shù)蝌衔。
在主隊列中接收事件或者數(shù)據(jù)榛泛,然后用后臺操作隊列來執(zhí)行實際操作,然后回到主隊列去傳遞結(jié)果噩斟,遵循這樣的原則來編寫盡量簡單的并行代碼曹锨,高效。