文字源自對以下文章的摘抄:
- threading-programming-guide筆記一
- threading-programming-guide筆記二
- threading-programming-guide筆記三
- threading-programming-guide筆記四
感謝原作者。
這里摘抄眉睹,只為學(xué)習(xí)目的迟郎,以便日后再復(fù)習(xí)。
一、OS X和iOS中提供的不那么底層的實現(xiàn)多任務(wù)并發(fā)執(zhí)行的解決方案:
Operation object:該技術(shù)出現(xiàn)在OS X 10.5中善绎,通過將要執(zhí)行的任務(wù)封裝成操作對象的方式實現(xiàn)任務(wù)在多線程中執(zhí)行旗扑。任務(wù)可以理解為你要想執(zhí)行的一段代碼。在這個操作對象中不光包含要執(zhí)行的任務(wù)曼库,還包含線程管理的內(nèi)容区岗,使用時通常與操作隊列對象聯(lián)合使用,操作隊列對象會管理操作對象如何使用線程毁枯,所以我們只需要關(guān)心要執(zhí)行的任務(wù)本身即可慈缔。
GCD:該技術(shù)出現(xiàn)在OS X 10.6中,它與Operation Object的初衷類似种玛,就是讓開發(fā)者只關(guān)注要執(zhí)行的任務(wù)本身藐鹤,而不需要去關(guān)注線程的管理。你只需要創(chuàng)建好任務(wù)赂韵,然后將任務(wù)添加到一個工作隊列里即可娱节,該工作隊列會根據(jù)當(dāng)前CPU性能及內(nèi)核的負(fù)載情況,將任務(wù)安排到合適的線程中去執(zhí)行祭示。
Idle-time notification:該技術(shù)主要用于處理優(yōu)先級相對比較低肄满、執(zhí)行時間比較短的任務(wù),讓應(yīng)用程序在空閑的時候執(zhí)行這類任務(wù)质涛。Cocoa框架提供NSNotificationQueue對象處理空閑時間通知悄窃,通過使用NSPostWhenIdle選項,向隊列發(fā)送空閑時間通知的請求蹂窖。
Asynchronous functions:系統(tǒng)中有一些支持異步的函數(shù)轧抗,可以自動讓你的代碼并行執(zhí)行。這些異步函數(shù)可能通過應(yīng)用程序的守護進(jìn)程或者自定義的線程執(zhí)行你的代碼瞬测,與主進(jìn)程或主線程分離横媚,達(dá)到并行執(zhí)行任務(wù)的功能纠炮。
Timers:我們也可以在應(yīng)用程序主線程中使用定時器去執(zhí)行一些比較輕量級的、有一定周期性的任務(wù)灯蝴。
Separate processes:雖然通過另起一個進(jìn)程比線程更加重量級恢口,但是在某些情況下要比使用線程更好一些,比如你需要的執(zhí)行的任務(wù)和你的應(yīng)用程序在展現(xiàn)數(shù)據(jù)和使用方面沒有什么關(guān)系穷躁,但是可以優(yōu)化你的應(yīng)用程序的運行環(huán)境耕肩,或者提高應(yīng)用程序獲取數(shù)據(jù)的效率等。
在應(yīng)用程序?qū)用嫖侍叮还苁鞘裁雌脚_猿诸,線程的運行方式都是大體相同的,在線程的運行過程中一般都會經(jīng)歷三種狀態(tài)狡忙,即運行中梳虽、準(zhǔn)備運行、阻塞灾茁。
二窜觉、RunLoop
參考:threading-programming-guide筆記三
簡單的來說,RunLoop用于管理和監(jiān)聽異步添加到線程中的事件北专,當(dāng)有事件輸入時禀挫,系統(tǒng)喚醒線程并將事件分派給RunLoop,當(dāng)沒有需要處理的事件時拓颓,RunLoop會讓線程進(jìn)入休眠狀態(tài)语婴。這樣就能讓線程常駐在進(jìn)程中,而不會過多的消耗系統(tǒng)資源录粱,達(dá)到有事做事,沒事睡覺的效果画拾。
Run Loop在線程中的主要作用就是幫助線程常駐在進(jìn)程中啥繁,并且不會過多消耗資源。所以說Run Loop在二級線程中也不是必須需要的青抛,要根據(jù)該線程執(zhí)行的任務(wù)類型以及在整個應(yīng)用中擔(dān)任何作用而決定是否需要使用Run Loop旗闽。比如說,如果你創(chuàng)建一個二級線程只是為了執(zhí)行一個不會頻繁執(zhí)行的一次性任務(wù)蜜另,或者需要執(zhí)行很長時間的任務(wù)适室,那么可能就不需要使用Run Loop了。如果你需要一個線程執(zhí)行周期性的定時任務(wù)举瑰,或者需要較為頻繁的與主線程之間進(jìn)行交互捣辆,那么就需要使用Run Loop。
使用Run Loop的情況大概有以下四點:
- 通過基于端口或自定義的數(shù)據(jù)源與其他線程進(jìn)行交互此迅。
- 在線程中執(zhí)行定時事件源的任務(wù)汽畴。
- 使用Cocoa框架提供的performSelector…系列方法旧巾。
- 在線程中執(zhí)行較為頻繁的,具有周期性的任務(wù)忍些。
Run Loop對象
要想操作配置Run Loop鲁猩,那自然需要通過Run Loop對象來完成,它提供了一系列接口罢坝,可幫助我們便捷的添加Input sources廓握、timers以及觀察者。較高級別的Cocoa框架提供了NSRunLoop類嘁酿,較底層級別的Core Foundation框架提供了指向CFRunloopRef的指針隙券。
獲取Run Loop對象
在Cocoa和Core Foundation框架中都沒有提供創(chuàng)建Run Loop的方法,只有從當(dāng)前線程獲取Run Loop的方法:
- 在Cocoa框架中痹仙,NSRunLoop類提供了類方法currentRunLoop()獲取NSRunLoop對象是尔。
該方法是獲取當(dāng)前線程中已存在的Run Loop,如果不存在开仰,那其實還是會創(chuàng)建一個Run Loop對象返回拟枚,只是Cocoa框架沒有向我們暴露該接口。 - 在Core Foundation框架中提供了CFRunLoopGetCurrent()函數(shù)獲取CFRunLoop對象
雖然這兩個Run Loop對象并不完全等價众弓,它們之間還是可以轉(zhuǎn)換的恩溅,我們可以通過NSRunLoop對象提供的getCFRunLoop()方法獲取CFRunLoop對象。因為NSRunLoop和CFRunLoop指向的都是當(dāng)前線程中同一個Run Loop谓娃,所以在使用時它們可以混用脚乡,比如說要給Run Loop添加觀察者時就必須得用CFRunLoop了。
配置Run Loop觀察者
可以向Run Loop中添加各種事件源和觀察者滨达,這里事件源是必填項奶稠,也就是說Run Loop中至少要有一種事件源,不論是Input source還是timer捡遍,如果Run Loop中沒有事件源的話锌订,那么在啟動Run Loop后就會立即退出。而觀察者是可選項画株,如果沒有監(jiān)控Run Loop各運行狀態(tài)的需求辆飘,可以不配置觀察者。
啟動Run Loop
在啟動Run Loop前務(wù)必要保證已添加一種類型的事件源谓传。在Cocoa框架和Core Foundation框架中啟動Run Loop大體有三種形式蜈项,分別是無條件啟動、設(shè)置時間限制啟動续挟、指定特定模式啟動紧卒。
1.無條件啟動
NSRunLoop對象的run()方法和Core Foundation框架中的CFRunLoopRun()函數(shù)都是無條件啟動Run Loop的方式。這種方式雖然是最簡單的啟動方式诗祸,但也是最不推薦使用的一個方式常侦,因為這種方式將Run Loop置于一個永久運行并且不可控的狀態(tài)浇冰,它使Run Loop只能在默認(rèn)模式下運行,無法給Run Loop設(shè)置特定的或自定義的模式聋亡,而且以這種模式啟動的Run Loop只能通過CFRunLoopStop(_ rl: CFRunLoop!)函數(shù)強制停止肘习。
2.設(shè)置時間限制啟動
該方式對應(yīng)的方法是NSRunLoop對象的runUntilDate(_ limitDate: NSDate)方法,在啟動Run Loop時設(shè)置超時時間坡倔,一旦超時那么Run Loop則自動退出漂佩。該方法的好處是可以在循環(huán)中反復(fù)啟動Run Loop處理相關(guān)任務(wù),而且可控制運行時長罪塔。
3.指定特定模式啟動
該方式對應(yīng)的方法是NSRunLoop對象的runMode(_ mode: String, beforeDate limitDate: NSDate)方法和Core Foundation框架的CFRunLoopRunInMode(_ mode: CFString!, _ seconds: CFTimeInterval, _ returnAfterSourceHandled: Bool)函數(shù)投蝉。前者有兩個參數(shù),第一個參數(shù)是Run Loop模式征堪,第二個參數(shù)仍然是超時時間瘩缆,該方法使Run Loop只處理指定模式中的事件源事件,當(dāng)處理完事件或超時Run Loop會退出佃蚜,該方法的返回值類型是Bool庸娱,如果返回true則表示Run Loop啟動成功,并分派執(zhí)行了任務(wù)或者達(dá)到超時時間谐算,若返回false則表示Run Loop啟動失敗熟尉。后者有三個參數(shù),前兩個參數(shù)的作用一樣洲脂,第三個參數(shù)的意思是Run Loop是否在執(zhí)行完任務(wù)后就退出斤儿,如果設(shè)置為false,那么代表Run Loop在執(zhí)行完任務(wù)后不退出恐锦,而是一直等到超時后才退出往果。該方法返回Run Loop的退出狀態(tài):
- CFRunLoopRunResult.Finished:表示Run Loop已分派執(zhí)行完任務(wù),并且再無任務(wù)執(zhí)行的情況下退出一铅。
- CFRunLoopRunResult.Stopped:表示Run Loop通過CFRunLoopStop(_ rl: CFRunLoop!)函數(shù)強制退出陕贮。
- CFRunLoopRunResult.TimedOut:表示Run Loop因為超時時間到而退出。
- CFRunLoopRunResult.HandledSource:表示Run Loop已執(zhí)行完任務(wù)而退出馅闽,改狀態(tài)只有在returnAfterSourceHandled設(shè)置為true時才會出現(xiàn)飘蚯。
退出Run Loop
退出Run Loop的方式總體來說有三種:
- 啟動Run Loop時設(shè)置超時時間馍迄。
- 強制退出Run Loop福也。
- 移除Run Loop中的事件源,從而使Run Loop退出攀圈。
第一種方式是推薦使用的方式暴凑,因為可以給Run Loop設(shè)置可控的運行時間,讓它執(zhí)行完所有的任務(wù)以及給觀察者發(fā)送通知赘来。第二種強制退出Run Loop主要是應(yīng)對無條件啟動Run Loop的情況现喳。第三種方式是最不推薦的方式凯傲,雖然在理論上說當(dāng)Run Loop中沒有任何數(shù)據(jù)源時會立即退出,但是在實際情況中我們創(chuàng)建的二級線程除了執(zhí)行我們指定的任務(wù)外嗦篱,有可能系統(tǒng)還會讓其執(zhí)行一些系統(tǒng)層面的任務(wù)冰单,而且這些任務(wù)我們一般無法知曉,所以用這種方式退出Run Loop往往會存在延遲退出灸促。
Run Loop對象的線程安全性
Run Loop對象的線程安全性取決于我們使用哪種API去操作诫欠。Core Foundation框架中的CFRunLoop對象是線程安全的,我們可以在任何線程中使用浴栽。Cocoa框架的NSRunLoop對象是線程不安全的荒叼,我們必須在擁有Run Loop的當(dāng)前線程中操作Run Loop,如果操作了不屬于當(dāng)前線程的Run loop典鸡,會導(dǎo)致異常和各種潛在的問題發(fā)生被廓。
自定義Run Loop事件源
Cocoa框架因為是較為高層的框架,所以沒有提供操作較為底層的Run Loop事件源相關(guān)的接口和對象萝玷,所以我們只能使用Core Foundation框架中的對象和函數(shù)創(chuàng)建事件源并給Run Loop設(shè)置事件源嫁乘。
1.創(chuàng)建Run Loop事件源對象
創(chuàng)建事件源的方法:
func CFRunLoopSourceCreate(_ allocator: CFAllocator!, _ order: CFIndex, _ context: UnsafeMutablePointer<CFRunLoopSourceContext>) -> CFRunLoopSource!
- allocator:該參數(shù)為對象內(nèi)存分配器,一般使用默認(rèn)的分配器kCFAllocatorDefault间护。
- order:事件源優(yōu)先級亦渗,當(dāng)Run Loop中有多個接收相同事件的事件源被標(biāo)記為待執(zhí)行時,那么就根據(jù)該優(yōu)先級判斷汁尺,0為最高優(yōu)先級別法精。
- context:事件源上下文。
Run Loop事件源上下文很重要痴突,我們來看看它的結(jié)構(gòu):
struct CFRunLoopSourceContext {
var version: CFIndex
var info: UnsafeMutablePointer<Void>
var retain: ((UnsafePointer<Void>) -> UnsafePointer<Void>)!
var release: ((UnsafePointer<Void>) -> Void)!
var copyDescription: ((UnsafePointer<Void>) -> Unmanaged<CFString>!)!
var equal: ((UnsafePointer<Void>, UnsafePointer<Void>) -> DarwinBoolean)!
var hash: ((UnsafePointer<Void>) -> CFHashCode)!
var schedule: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!
var cancel: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!
var perform: ((UnsafeMutablePointer<Void>) -> Void)!
init()
init(version version: CFIndex, info info: UnsafeMutablePointer<Void>, retain retain: ((UnsafePointer<Void>) -> UnsafePointer<Void>)!, release release: ((UnsafePointer<Void>) -> Void)!, copyDescription copyDescription: ((UnsafePointer<Void>) -> Unmanaged<CFString>!)!, equal equal: ((UnsafePointer<Void>, UnsafePointer<Void>) -> DarwinBoolean)!, hash hash: ((UnsafePointer<Void>) -> CFHashCode)!, schedule schedule: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!, cancel cancel: ((UnsafeMutablePointer<Void>, CFRunLoop!, CFString!) -> Void)!, perform perform: ((UnsafeMutablePointer<Void>) -> Void)!)
}
- version:事件源上下文的版本搂蜓,必須設(shè)置為0。
- info:上下文中retain辽装、release帮碰、copyDescription、equal拾积、hash殉挽、schedule、cancel拓巧、perform這八個回調(diào)函數(shù)所有者對象的指針斯碌。
- schedule:該回調(diào)函數(shù)的作用是將該事件源與給它發(fā)送事件消息的線程進(jìn)行關(guān)聯(lián),也就是說如果主線程想要給該事件源發(fā)送事件消息肛度,那么首先主線程得能獲取到該事件源傻唾。
- cancel:該回調(diào)函數(shù)的作用是使該事件源失效。
- perform:該回調(diào)函數(shù)的作用是執(zhí)行其他線程或當(dāng)前線程給該事件源發(fā)來的事件消息承耿。
將事件源添加至Run Loop
事件源創(chuàng)建好之后冠骄,接下來就是將其添加到指定某個模式的Run Loop中伪煤,我們來看看這個方法:
func CFRunLoopAddSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFString!)
- rl:希望添加事件源的Run Loop對象,類型是CFRunLoop凛辣。
- source:我們創(chuàng)建好的事件源抱既。
- mode:Run Loop的模式。
標(biāo)記事件源及喚醒Run Loop
func CFRunLoopSourceSignal(_ source: CFRunLoopSource!)
func CFRunLoopWakeUp(_ rl: CFRunLoop!)
這里需要注意的是喚醒Run Loop并不等價與啟動Run Loop扁誓,因為啟動Run Loop時需要對Run Loop進(jìn)行模式蝙砌、時限的設(shè)置,而喚醒Run Loop只是當(dāng)已啟動的Run Loop休眠時重新讓其運行跋理。
三择克、線程的資源消耗
線程的資源消耗主要分為三類,一類是內(nèi)存空間的消耗前普、一類是創(chuàng)建線程消耗的時間肚邢、另一類是對開發(fā)人員開發(fā)成本的消耗。
內(nèi)存空間的消耗又分為兩部分拭卿,一部分是內(nèi)核內(nèi)存空間骡湖,另一部分是應(yīng)用程序使用的內(nèi)存空間,每個線程在創(chuàng)建時就會申請這兩部分的內(nèi)存空間峻厚。申請內(nèi)核內(nèi)存空間是用來存儲管理和協(xié)調(diào)線程的核心數(shù)據(jù)結(jié)構(gòu)的响蕴,而申請應(yīng)用程序的內(nèi)存空間是用來存儲線程棧和一些初始化數(shù)據(jù)的。對于用戶級別的二級線程來說惠桃,對應(yīng)用程序內(nèi)存空間的消耗是可以配置的浦夷,比如線程棧的空間大小等。
下面是兩種內(nèi)存空間通常的消耗情況:
- 內(nèi)核內(nèi)存空間:主要存儲線程的核心數(shù)據(jù)結(jié)構(gòu)辜王,每個線程大約會占用1KB的空間劈狐。
- 應(yīng)用程序內(nèi)存空間:主要存儲線程棧和初始化數(shù)據(jù),主線程在OS X中大約占8MB空間呐馆,在iOS中大約占1MB肥缔。二級線程在兩種系統(tǒng)中通常占大約512KB,但是上面提到二級線程在這塊是可以配置的汹来,所以可配置的最小空間為16KB续膳,而且配置的空間大小必須是4KB的倍數(shù)。
注意:二級線程在創(chuàng)建時只是申請了內(nèi)存程序空間收班,但還并沒有真正分配給二級線程坟岔,只有當(dāng)二級線程執(zhí)行代碼需要空間時才會真正分配。
四闺阱、創(chuàng)建線程
說到創(chuàng)建線程炮车,就得說說線程的兩種類型舵变,Joinable和Detach酣溃。Joinable類型的線程可以被其他線程回收其資源和終止瘦穆。舉個例子,如果一個Joinable的線程與主線程結(jié)合赊豌,那么當(dāng)主線程準(zhǔn)備結(jié)束而該二級線程還沒有結(jié)束的時候扛或,主線程會被阻塞等待該二級線程,當(dāng)二級線程結(jié)束后由主線程回收其占用資源并將其關(guān)閉碘饼。如果在主線程還沒有結(jié)束時熙兔,該二級線程結(jié)束了,那么它不但不會關(guān)閉艾恼,而且資源也不會被系統(tǒng)收回住涉,只是等待主線程處理。而Detach的線程則相反钠绍,會自行結(jié)束關(guān)閉線程并且有系統(tǒng)回收其資源舆声。
五、線程屬性配置
線程也是具有若干屬性的柳爽,自然一些屬性也是可配置的媳握,在啟動線程之前我們可以對其進(jìn)行配置,比如線程占用的內(nèi)存空間大小磷脯、線程持久層中的數(shù)據(jù)蛾找、設(shè)置線程類型、優(yōu)先級等赵誓。
1打毛、 配置線程的棧空間大小
- Cocoa框架:在OS X v10.5之后的版本和iOS2.0之后的版本中俩功,我們可以通過修改NSThread類的stackSize屬性隘冲,改變二級線程的線程棧大小,不過這里要注意的是該屬性的單位是字節(jié)绑雄,并且設(shè)置的大小必須得是4KB的倍數(shù)展辞。
- POSIX API:通過pthread_attr_- setstacksize函數(shù)給線程屬性pthread_attr_t結(jié)構(gòu)體設(shè)置線程棧大小,然后在使用pthread_create函數(shù)創(chuàng)建線程時將線程屬性傳入即可万牺。
注意:在使用Cocoa框架的前提下修改線程棧時罗珍,不能使用NSThread的detachNewThreadSelector: toTarget:withObject:方法,因為上文中說過脚粟,該方法先創(chuàng)建線程覆旱,即刻便啟動了線程,所以根本沒有機會修改線程屬性核无。
2扣唱、配置線程存儲字典
每一個線程,在整個生命周期里都會有一個字典,以key-value的形式存儲著在線程執(zhí)行過程中你希望保存下來的各種類型的數(shù)據(jù)噪沙,比如一個常駐線程的運行狀態(tài)炼彪,線程可以在任何時候訪問該字典里的數(shù)據(jù)。
在Cocoa框架中正歼,可以通過NSThread類的threadDictionary屬性辐马,獲取到NSMutableDictionary類型對象,然后自定義key值局义,存入任何里先儲存的對象或數(shù)據(jù)喜爷。如果使用POSIX線程,可以使用pthread_setspecific和pthread_getspecific函數(shù)設(shè)置獲取線程字典萄唇。
3檩帐、配置線程類型
在上文中提到過,線程有Joinable和Detached類型另萤,大多數(shù)非底層的線程默認(rèn)都是Detached類型的轿塔,相比Joinable類型的線程來說薛匪,Detached類型的線程不用與其他線程結(jié)合捶朵,并且在執(zhí)行完任務(wù)后可自動被系統(tǒng)回收資源,而且主線程不會因此而阻塞脉课,這著實要方便許多目养。
使用NSThread創(chuàng)建的線程默認(rèn)都是Detached類型俩由,而且似乎也不能將其設(shè)置為Joinable類型。而使用POSIX API創(chuàng)建的線程則默認(rèn)為Joinable類型癌蚁,而且這也是唯一創(chuàng)建Joinable類型線程的方式幻梯。通過POSIX API可以在創(chuàng)建線程前通過函數(shù)pthread_attr_setdetachstate更新線程屬性,將其設(shè)置為不同的類型努释,如果線程已經(jīng)創(chuàng)建碘梢,那么可以使用pthread_detach函數(shù)改變其類型。Joinable類型的線程還有一個特性伐蒂,那就是在終止之前可以將數(shù)據(jù)傳給與之相結(jié)合的線程煞躬,從而達(dá)到線程之間的交互。即將要終止的線程可以通過pthread_exit函數(shù)傳遞指針或者任務(wù)執(zhí)行的結(jié)果逸邦,然后與之結(jié)合的線程可以通過pthread_join函數(shù)接受數(shù)據(jù)恩沛。
雖然通過POSIX API創(chuàng)建的線程使用和管理起來較為復(fù)雜和麻煩,但這也說明這種方式更為靈活缕减,更能滿足不同的使用場景和需求雷客。比如當(dāng)執(zhí)行一些關(guān)鍵的任務(wù),不能被打斷的任務(wù)桥狡,像執(zhí)行I/O操作之類搅裙。
4皱卓、 設(shè)置線程優(yōu)先級
不論是通過NSThread創(chuàng)建線程還是通過POSIX API創(chuàng)建線程,他們都提供了設(shè)置線程優(yōu)先級的方法部逮。我們可以通過NSThread的類方法setThreadPriority:設(shè)置優(yōu)先級娜汁,因為線程的優(yōu)先級由0.0~1.0表示,所以設(shè)置優(yōu)先級時也一樣甥啄。我們也可以通過pthread_setschedparam函數(shù)設(shè)置線程優(yōu)先級。
注意:設(shè)置線程的優(yōu)先級時可以在線程運行時設(shè)置炬搭。
雖然我們可以調(diào)節(jié)線程的優(yōu)先級蜈漓,但不到必要時還是不建議調(diào)節(jié)線程的優(yōu)先級。因為一旦調(diào)高了某個線程的優(yōu)先級宫盔,與低優(yōu)先級線程的優(yōu)先等級差距太大融虽,就有可能導(dǎo)致低優(yōu)先級線程永遠(yuǎn)得不到運行的機會,從而產(chǎn)生性能瓶頸灼芭。比如說有兩個線程A和B有额,起初優(yōu)先級相差無幾,那么在執(zhí)行任務(wù)的時候都會相繼無序的運行彼绷,如果將線程A的優(yōu)先級調(diào)高巍佑,并且當(dāng)線程A不會因為執(zhí)行的任務(wù)而阻塞時,線程B就可能一直不能運行寄悯,此時如果線程A中執(zhí)行的任務(wù)需要與線程B中任務(wù)進(jìn)行數(shù)據(jù)交互萤衰,而遲遲得不到線程B中的結(jié)果,此時線程A就會被阻塞猜旬,那么程序的性能自然就會產(chǎn)生瓶頸脆栋。