學(xué)習(xí)筆記_objccn_并發(fā)編程

并發(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é)果噩斟,遵循這樣的原則來編寫盡量簡單的并行代碼曹锨,高效。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剃允,一起剝皮案震驚了整個濱河市沛简,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌斥废,老刑警劉巖椒楣,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異牡肉,居然都是意外死亡捧灰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門统锤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來毛俏,“玉大人,你說我怎么就攤上這事饲窿』涂埽” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵逾雄,是天一觀的道長阀溶。 經(jīng)常有香客問我,道長鸦泳,這世上最難降的妖魔是什么银锻? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮辽故,結(jié)果婚禮上徒仓,老公的妹妹穿的比我還像新娘腐碱。我一直安慰自己誊垢,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布症见。 她就那樣靜靜地躺著喂走,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谋作。 梳的紋絲不亂的頭發(fā)上芋肠,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音遵蚜,去河邊找鬼帖池。 笑死奈惑,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的睡汹。 我是一名探鬼主播肴甸,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼囚巴!你這毒婦竟也來了原在?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤彤叉,失蹤者是張志新(化名)和其女友劉穎庶柿,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秽浇,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡浮庐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了柬焕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兔辅。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖击喂,靈堂內(nèi)的尸體忽然破棺而出维苔,到底是詐尸還是另有隱情,我是刑警寧澤懂昂,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布介时,位于F島的核電站,受9級特大地震影響凌彬,放射性物質(zhì)發(fā)生泄漏沸柔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一铲敛、第九天 我趴在偏房一處隱蔽的房頂上張望褐澎。 院中可真熱鬧,春花似錦伐蒋、人聲如沸工三。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俭正。三九已至,卻和暖如春焙畔,著一層夾襖步出監(jiān)牢的瞬間掸读,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留儿惫,地道東北人澡罚。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像肾请,于是被迫代替她去往敵國和親始苇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容