目前在 iOS 和 OS X 中有兩套先進的同步 API 可供我們使用:NSOperation 和 GCD ,。其中 GCD 是基于 C 的底層的 API ,而 NSOperation 則是 GCD 實現(xiàn)的 Objective-C API,。 雖然 NSOperation 是基于 GCD 實現(xiàn)的,, 但是并不意味著它是一個 GCD 的 “dumbed-down” 版本, 相反,,我們可以用NSOperation 輕易的實現(xiàn)一些 GCD 要寫大量代碼的事情,。 因此, NSOperationQueue 是被推薦使用的,, 除非你遇到了 NSOperationQueue 不能實現(xiàn)的問題,。 1. 為什么優(yōu)先使用NSOperationQueue而不是GCD曾經(jīng)我有一段時間我非常喜歡使用GCD來進行并發(fā)編程,因為雖然它是C的api,,但是使用起來卻非常簡單和方便, 不過這樣也就容易使開發(fā)者忘記并發(fā)編程中的許多注意事項和陷阱,。 比如你可能寫過類似這樣的代碼(這樣來請求網(wǎng)絡(luò)數(shù)據(jù)):
沒錯,它是可以正常的工作,,但是有個致命的問題:這個任務(wù)是無法取消的 因此我們不推薦這種寫法來從網(wǎng)絡(luò)拉取數(shù)據(jù),。 操作隊列(operation queue)是由 GCD 提供的一個隊列模型的 Cocoa 抽象。GCD 提供了更加底層的控制,而操作隊列則在 GCD 之上實現(xiàn)了一些方便的功能,,這些功能對于 app 的開發(fā)者來說通常是最好最安全的選擇,。NSOperationQueue相對于GCD來說有以下優(yōu)點:
名詞: 本文中提到的 “任務(wù)”,, “操作” 即代表要再NSOperation中執(zhí)行的事情,。 2. Operation Queues的使用2.1 NSOperationQueue
我們可以通過設(shè)置 2.2 NSOperation
你可以使用系統(tǒng)提供的一些現(xiàn)成的
使用
如果你希望擁有更多的控制權(quán),,或者想在一個操作中可以執(zhí)行異步任務(wù),那么就重寫
當實現(xiàn)了start方法時,,默認會執(zhí)行start方法,,而不執(zhí)行main方法
為了讓操作隊列能夠捕獲到操作的改變,需要將狀態(tài)的屬性以配合 需要手動管理的狀態(tài)有:
手動的發(fā)送
為了能使用操作隊列所提供的取消功能,,你需要在長時間操作中時不時地檢查
3. RunLoop在cocoa中講到多線程,,那么就不得不講到RunLoop。 在ios/mac的編碼中,,我們似乎不需要過多關(guān)心代碼是如何執(zhí)行的,,一切仿佛那么自然。比如我們知道當滑動手勢時,,tableView就會滾動,,啟動一個NSTimer之后,timer的方法就會定時執(zhí)行,, 但是為什么呢,其實是RunLoop在幫我們做這些事情:分發(fā)消息,。 3.1 什么是RunLoop你應(yīng)該看過這樣的偽代碼解釋ios的app中main函數(shù)做的事情:
也應(yīng)該看過這樣的代碼用來阻塞一個線程:
或許你感覺到他們有些神奇,,希望我的解釋能讓你明白一些. 我們先思考一個問題: 當我們打開一個IOS應(yīng)用之后,什么也不做,,這時候看起來是沒有代碼在執(zhí)行的,,為什么應(yīng)用沒有退出呢? 我們在寫c的簡單的只有一個main函數(shù)的程序時就知道,,當main的代碼執(zhí)行完,,沒有事情可做的時候,程序就執(zhí)行完畢退出了,。而我們IOS的應(yīng)用是如何做到在沒有事情做的時候維持應(yīng)用的運行的呢? 那就是RunLoop,。 RunLoop的字面意思就是“運行回路”,聽起來像是一個循環(huán),。實際它就是一個循環(huán),,它在循環(huán)監(jiān)聽著事件源,把消息分發(fā)給線程來執(zhí)行,。RunLoop并不是線程,,也不是并發(fā)機制,但是它在線程中的作用至關(guān)重要,,它提供了一種異步執(zhí)行代碼的機制,。 3.2 事件源
由圖中可以看出NSRunLoop只處理兩種源:輸入源、時間源,。而輸入源又可以分為: 3.2.1 NSPort 基于端口的源Cocoa和 Core Foundation 為使用端口相關(guān)的對象和函數(shù)創(chuàng)建的基于端口的源提供了內(nèi)在支持,。Cocoa中你從不需要直接創(chuàng)建輸入源,。你只需要簡單的創(chuàng)建端口對象,并使用NSPort的方法將端口對象加入到run loop。端口對象會處理創(chuàng)建以及配置輸入源,。
NSPort一般分三種: 3.2.2 自定義輸入源在Core Foundation程序中,必須使用CFRunLoopSourceRef類型相關(guān)的函數(shù)來創(chuàng)建自定義輸入源,,接著使用回調(diào)函數(shù)來配置輸入源,。Core Fundation會在恰當?shù)臅r候調(diào)用回調(diào)函數(shù),處理輸入事件以及清理源,。常見的觸摸,、滾動事件等就是該類源,由系統(tǒng)內(nèi)部實現(xiàn),。 一般我們不會使用該種源,,第三種情況已經(jīng)滿足我們的需求 3.2.3 performSelector:OnThreadCocoa提供了可以在任一線程執(zhí)行函數(shù)(perform selector)的輸入源。和基于端口的源一樣,,perform selector請求會在目標線程上序列化,,減緩許多在單個線程上容易引起的同步問題。而和基于端口的源不同的是,,perform selector執(zhí)行完后會自動清除出run loop,。 此方法簡單實用,使用也更廣泛,。 3.2.4 定時源定時源就是NSTimer了,,定時源在預(yù)設(shè)的時間點同步地傳遞消息。因為Timer是基于RunLoop的,,也就決定了它不是實時的,。 3.3 RunLoop觀察者
我們可以通過創(chuàng)建
3.4 Run Loop ModesRunLoop對于上述四種事件源的監(jiān)視,,可以通過設(shè)置模式來決定監(jiān)視哪些源,。 RunLoop只會處理與當前模式相關(guān)聯(lián)的源,未與當前模式關(guān)聯(lián)的源則處于暫停狀態(tài),。 cocoa和Core Foundation預(yù)先定義了一些模式(Apple文檔翻譯):
我們也可以自定義模式,,可以參考 3.5 常見問題一:為什么TableView滑動時,Timer暫停了,?
我們做個測試: 在一個 viewController 的
也就是說,當前主線程的 RunLoop 正在以
常見的解決方案是把Timer“綁定”到
這樣這個Timer就可以和當前組中的兩種模式
注意: 由上面可以發(fā)現(xiàn)
上面的Log是一個間隔為 因此當我們用NSTimer來完成一些計時任務(wù)時,,如果需要比較精確的話,最好還是要比較“時間戳”,。 3.6 常見問題二:后臺的NSURLConnection不回調(diào),,Timer不運行
我們知道每個線程都有它的RunLoop, 我們可以通過 后臺線程的RunLoop沒有啟動的情況下的現(xiàn)象就是:“代碼執(zhí)行完,線程就結(jié)束被回收了”,。就像我們簡單的程序執(zhí)行完就退出了,。 所以如果我們希望在代碼執(zhí)行完成后還要保留線程等待一些異步的事件時,比如NSURLConnection和NSTimer,, 就需要手動啟動后臺線程的RunLoop,。
啟動RunLoop,我們需要設(shè)定RunLoop的模式,,我們可以設(shè)置
我們也可以設(shè)置其他模式運行,但是我們就需要把“事件源” “綁定”到該模式上:
3.7 問題三:本節(jié)開頭的例子為何可以阻塞線程
你應(yīng)該知道這樣一段代碼可以阻塞當前線程,,你可能會奇怪:RunLoop就是不停循環(huán)來檢測源的事件,,為什么還要加個 這是因為RunLoop的特性,,RunLoop會在沒有“事件源”可監(jiān)聽時休眠,。也就是說如果當前沒有合適的“源”被RunLoop監(jiān)聽,那么這步就跳過了,,不能起到阻塞線程的作用,,所以還是要加個while循環(huán)來維持。 同時注意:因為這段代碼可以阻塞線程,,所以請不要在主線程寫下這段代碼,,因為它很可能會導致界面卡住。 4. 線程安全講了這么多,,你是否已經(jīng)對并發(fā)編程已經(jīng)躍躍欲試了呢,? 但是并發(fā)編程一直都不是一個輕松的事情,使用并發(fā)編程會帶來許多陷阱,。哪怕你是一個很成熟的程序員和架構(gòu)師,,也很難避免線程安全的問題;使用的越多,,出錯的可能就越大,,因此可以不用多線程就不要使用。 關(guān)于并發(fā)編程的不可預(yù)見性有一個非常有名的例子:在1995年,, NASA (美國宇航局)發(fā)送了開拓者號火星探測器,,但是當探測器成功著陸在我們紅色的鄰居星球后不久,任務(wù)嘎然而止,,火星探測器莫名其妙的不停重啟,,在計算機領(lǐng)域內(nèi),,遇到的這種現(xiàn)象被定為為優(yōu)先級反轉(zhuǎn),也就是說低優(yōu)先級的線程一直阻塞著高優(yōu)先級的線程,。在這里我們想說明的是,,即使擁有豐富的資源和大量優(yōu)秀工程師的智慧,并發(fā)也還是會在不少情況下反咬你你一口,。 4.1 資源共享和資源饑餓并發(fā)編程中許多問題的根源就是在多線程中訪問共享資源,。資源可以是一個屬性、一個對象,,通用的內(nèi)存,、網(wǎng)絡(luò)設(shè)備或者一個文件等等。在多線程中任何一個共享的資源都可能是一個潛在的沖突點,,你必須精心設(shè)計以防止這種沖突的發(fā)生,。 一般我們通過鎖來解決資源共享的問題,也就是可以通過對資源加鎖保證同時只有一個線程訪問資源 4.1.1 互斥鎖互斥訪問的意思就是同一時刻,,只允許一個線程訪問某個特定資源,。為了保證這一點,每個希望訪問共享資源的線程,,首先需要獲得一個共享資源的互斥鎖,。 對資源加鎖會引發(fā)一定的性能代價。 4.1.2 原子性從語言層面來說,,在 Objective-C 中將屬性以 atomic 的形式來聲明,,就能支持互斥鎖了。事實上在默認情況下,,屬性就是 atomic 的,。將一個屬性聲明為 atomic 表示每次訪問該屬性都會進行隱式的加鎖和解鎖操作,。雖然最把穩(wěn)的做法就是將所有的屬性都聲明為 atomic,,但是加解鎖這也會付出一定的代價。 4.1.3 死鎖互斥鎖解決了競態(tài)條件的問題,,但很不幸同時這也引入了一些其他問題,,其中一個就是死鎖。當多個線程在相互等待著對方的結(jié)束時,,就會發(fā)生死鎖,,這時程序可能會被卡住。 比如下面的代碼:
再比如:
上面兩個例子也可以說明 當你的代碼有死鎖的可能時,它就會發(fā)生 4.1.4 資源饑餓當你認為已經(jīng)足夠了解并發(fā)編程面臨的問題時,,又出現(xiàn)了一個新的問題,。鎖定的共享資源會引起讀寫問題,。大多數(shù)情況下,限制資源一次只能有一個線程進行讀取訪問其實是非常浪費的,。因此,,在資源上沒有寫入鎖的時候,持有一個讀取鎖是被允許的,。這種情況下,,如果一個持有讀取鎖的線程在等待獲取寫入鎖的時候,其他希望讀取資源的線程則因為無法獲得這個讀取鎖而導致資源饑餓的發(fā)生,。 4.2 優(yōu)先級反轉(zhuǎn)優(yōu)先級反轉(zhuǎn)是指程序在運行時低優(yōu)先級的任務(wù)阻塞了高優(yōu)先級的任務(wù),,有效的反轉(zhuǎn)了任務(wù)的優(yōu)先級。GCD提供了3種級別的優(yōu)先級隊列,,分別是Default, High, Low,。 高優(yōu)先級和低優(yōu)先級的任務(wù)之間共享資源時,就可能發(fā)生優(yōu)先級反轉(zhuǎn),。當?shù)蛢?yōu)先級的任務(wù)獲得了共享資源的鎖時,,該任務(wù)應(yīng)該迅速完成,并釋放掉鎖,,這樣高優(yōu)先級的任務(wù)就可以在沒有明顯延時的情況下繼續(xù)執(zhí)行,。然而高優(yōu)先級任務(wù)會在低優(yōu)先級的任務(wù)持有鎖的期間被阻塞。如果這時候有一個中優(yōu)先級的任務(wù)(該任務(wù)不需要那個共享資源),,那么它就有可能會搶占低優(yōu)先級任務(wù)而被執(zhí)行,,因為此時高優(yōu)先級任務(wù)是被阻塞的,所以中優(yōu)先級任務(wù)是目前所有可運行任務(wù)中優(yōu)先級最高的,。此時,,中優(yōu)先級任務(wù)就會阻塞著低優(yōu)先級任務(wù),導致低優(yōu)先級任務(wù)不能釋放掉鎖,,這也就會引起高優(yōu)先級任務(wù)一直在等待鎖的釋放,。如下圖:
使用不同優(yōu)先級的多個隊列聽起來雖然不錯,但畢竟是紙上談兵,。它將讓本來就復雜的并行編程變得更加復雜和不可預(yù)見,。因此我們寫代碼的時候最好只用Default優(yōu)先級的隊列,不要使用其他隊列來讓問題復雜化,。 關(guān)于dispatch_queue的底層線程安全設(shè)計可參考:底層并發(fā) API 5. 總結(jié)本文主要講了 NSOperationQueue,、 NSRunLoop、 和線程安全等三大塊內(nèi)容,。 希望可以幫助你理解 NSOperation的使用,, NSRunLoop的作用, 還有并發(fā)編程帶來的復雜性和相關(guān)問題,。 并發(fā)實際上是一個非常棒的工具,。它充分利用了現(xiàn)代多核 CPU 的強大計算能力,。但是因為它的復雜性,所以我們盡量使用高級的API,,盡量寫簡單的代碼,,讓并發(fā)模型保持簡單; 這樣可以寫出高效,、結(jié)構(gòu)清晰,、且安全的代碼。 |
|