這篇文章來(lái)自 @我就叫Sunny怎么了 推薦的Mike Ash的博客,主要是講如何自己動(dòng)手實(shí)現(xiàn)dispatch queue的基本功能。翻譯過(guò)程中個(gè)別地方稍作調(diào)整。初次翻譯,歡迎糾正!
原文地址:[https://www.mikeash.com/pyblog/friday-qa-2015-09-04-lets-build-dispatch_queue.html]
代碼地址:[https://github.com/mikeash/MADispatchQueue]
GCD是Apple近年來(lái)開(kāi)發(fā)的很棒的API之一,在"Lets Bulid"系列最新的一期中,我將對(duì)dispatch_queue的最基礎(chǔ)的特性做重新實(shí)現(xiàn),這個(gè)主題由Rob Rix推薦。
概覽
一個(gè)dispatch queue是一個(gè)由全局線程池支持的工作隊(duì)列。典型的,工作提交到一個(gè)隊(duì)列在后臺(tái)線程異步執(zhí)行。所有線程共享一個(gè)單一的后臺(tái)線程池,這能使系統(tǒng)更高效。
這是我要重現(xiàn)的這個(gè)API的本質(zhì)。我為了簡(jiǎn)單會(huì)忽略GCD提供的高級(jí)的特性。比如,完成在全局池中增加以及減少線程數(shù)量的大量工作,以及系統(tǒng)對(duì)CPU的利用。如果你有一堆任務(wù)占用了CPU并且你提交了其他任務(wù),GCD會(huì)避免為他創(chuàng)建其他的工作線程,因?yàn)榇藭r(shí)CPU使用率已經(jīng)達(dá)到100%,其他的線程工作會(huì)變得低效。我將跳過(guò)這點(diǎn),對(duì)線程的數(shù)量使用硬編碼。我也會(huì)略過(guò)其他的特性,像定位隊(duì)列和封閉并發(fā)隊(duì)列。
我們的目標(biāo)是關(guān)注dispatch queues的本質(zhì):串行和并發(fā),他們能同步或者異步的派發(fā)任務(wù),并且由一個(gè)共享的全局線程池支持。
接口:
GCD是一個(gè)C語(yǔ)言的API。雖然GCD對(duì)象已經(jīng)在最近的OS版本中轉(zhuǎn)換為OC的對(duì)象,但是API維持純粹的C(附加蘋(píng)果block的擴(kuò)展)。這是一個(gè)很棒的低層API,GCD提供了非常清晰的接口,但是為了完成我的目標(biāo),我寧愿用OC來(lái)重現(xiàn)。
OC類(lèi)名為MADispatchQueue
他只有4個(gè)調(diào)用方法:
一個(gè)獲取共享全局隊(duì)列的方法。GCD有多個(gè)不同優(yōu)先級(jí)的全局隊(duì)列,但是我們?yōu)榱撕?jiǎn)單只有一個(gè)。
一個(gè)初始化方法,為了能創(chuàng)建并發(fā)或者串行的隊(duì)列。
一個(gè)異步的dispatch調(diào)用
一個(gè)同步的dispatch調(diào)用
方法的聲明:
@interface MADispatchQueue : NSObject
+ (MADispatchQueue *)globalQueue;
- (id)initSerial: (BOOL)serial;
- (void)dispatchAsync: (dispatch_block_t)block;
- (void)dispatchSync: (dispatch_block_t)block;
@end
之后去完成他們所描述的要做的事情。
線程池接口
線程池有一個(gè)簡(jiǎn)單的接口支持隊(duì)列。他將會(huì)做一些實(shí)際運(yùn)行中的,已提交的任務(wù)的繁重工作。隊(duì)列能夠可靠的在一個(gè)正確的時(shí)機(jī)提交他們隊(duì)列中的任務(wù)。線程池有一個(gè)單一的任務(wù):提交一些工作來(lái)運(yùn)行。因此接口只有一個(gè)方法:
@interface MAThreadPool : NSObject
- (void)addBlock: (dispatch_block_t)block;
@end
由于這是核心,所以讓我們先實(shí)現(xiàn)他。
線程池的實(shí)現(xiàn)
先來(lái)看實(shí)例變量。線程池是能夠被多線程訪問(wèn)的,包括內(nèi)部和外部,并且需要線程安全。GCD脫離他自己的方法盡可能的使用快速原子化的操作,在我的重建中我堅(jiān)持使用老式的鎖。我需要這個(gè)鎖能夠等待以及發(fā)送信號(hào),不只是實(shí)施互斥,所以我使用了NSCondition而不是一個(gè)普通的NSLock。如果你對(duì)他不熟悉,可以理解為:NSCondition基本上就是一個(gè)鎖和一個(gè)單一條件變量的封裝。
NSCondition *_lock;
為了知道何時(shí)自旋向上的新增工作線程,我需要知道線程池里有多少線程,有多少實(shí)際在工作的,以及線程的最大數(shù)量:
NSUInteger _threadCount;
NSUInteger _activeThreadCount;
NSUInteger _threadCountLimit;
最終,有一串block去執(zhí)行。使用NSMutableArray,通過(guò)在末尾添加新的block以及從開(kāi)頭移除來(lái)模擬一個(gè)隊(duì)列。
NSMutableArray *_blocks;
初始化工作很簡(jiǎn)單。初始化鎖,初始化block數(shù)組,使用一個(gè)任意數(shù)量來(lái)設(shè)置線程的數(shù)量限制,這里用128:
- (id)init {
if((self = [super init])) {
_lock = [[NSCondition alloc] init];
_blocks = [[NSMutableArray alloc] init];
_threadCountLimit = 128;
}
return self;
}
工作的線程在一個(gè)簡(jiǎn)單的無(wú)限循環(huán)中運(yùn)行,直到blocks數(shù)組為空,狀態(tài)置為等待。一旦有可獲取的block,這個(gè)block會(huì)從數(shù)組中出列并執(zhí)行。當(dāng)我們這樣做的時(shí)候,將增加活動(dòng)的線程數(shù)量,那么在結(jié)束時(shí)需要減少數(shù)量。
- (void)workerThreadLoop: (id)ignore {
第一件事是獲取鎖,記住這必須在循環(huán)開(kāi)始之前。至于理由,在循環(huán)的結(jié)束時(shí)候你將會(huì)明白這點(diǎn)。
[_lock lock];
無(wú)限循環(huán):
while(1){
如果隊(duì)列為空,那么鎖是等待狀態(tài):
while([_blocks count] == 0) {
[_lock wait];
}
記住這個(gè)需要通過(guò)循環(huán)完成,而不是一個(gè)if語(yǔ)句。理由可參考:[https://en.wikipedia.org/wiki/Spurious_wakeup]
簡(jiǎn)單來(lái)說(shuō),wait這個(gè)狀態(tài)即便沒(méi)有signaled也有可能return,所以為了修正這種行為,當(dāng)wait return時(shí),需要重新檢驗(yàn)條件。
一旦block可以獲得,讓丫的出列:
dispatch_block_t block = [_blocks firstObject];
[_blocks removeObjectAtIndex: 0];
通過(guò)增加活動(dòng)線程數(shù)量來(lái)表明該線程正在活動(dòng):
_activeThreadCount++;
現(xiàn)在是時(shí)候執(zhí)行block了,但是我們必須先釋放鎖,否則我們做不到并發(fā),同時(shí)我們將會(huì)有各種各樣好玩的死鎖:
[_lock unlock];
鎖安全的放手后,執(zhí)行block:
block();
block結(jié)束后,是時(shí)候減少活動(dòng)線程的數(shù)量了。這必須結(jié)合鎖來(lái)完成,以避免資源競(jìng)爭(zhēng),循環(huán)的最后:
[_lock lock];
_activeThreadCount--;
}
}
現(xiàn)在你能看到為什么在循環(huán)的最上層要獲取鎖。循環(huán)中做的最后一件事是減少活動(dòng)線程的數(shù)量,這需要保持鎖的狀態(tài)。在循環(huán)的頂層第一件事是檢查block的隊(duì)列。通過(guò)在循環(huán)外執(zhí)行第一個(gè)鎖,后續(xù)重復(fù)的事情的所有操作能夠使用一個(gè)單一的鎖來(lái)操作,二不是鎖,解鎖,再鎖…
addBlock方法:
- (void)addBlock: (dispatch_block_t)block {
這里的每件事情都需要結(jié)合鎖的獲取來(lái)完成
[_lock lock];
第一個(gè)任務(wù)是添加一個(gè)新的block到block隊(duì)列:
[_blocks addObject: block];
如果有一個(gè)閑置的線程準(zhǔn)備取走這個(gè)block,那接下來(lái)沒(méi)什么可做的了。如果沒(méi)有足夠的閑置線程來(lái)執(zhí)行未完成的block,而且工作線程的數(shù)量還沒(méi)有到上限,那么是時(shí)候創(chuàng)建一個(gè)新的線程了:
NSUInteger idleThreads = _threadCount - _activeThreadCount;
if([_blocks count] > idleThreads && _threadCount < _threadCountLimit) {
[NSThread detachNewThreadSelector: @selector(workerThreadLoop:)
toTarget: self
withObject: nil];
_threadCount++;
}
現(xiàn)在一個(gè)工作線程啟動(dòng)的所有準(zhǔn)備工作已經(jīng)完成。 假設(shè)他們都是沉睡狀態(tài),喚醒一個(gè)
[_lock signal];
然后釋放鎖就完成了
[_lock unlock];
}
這為我們提供了一個(gè)線程池,來(lái)產(chǎn)出預(yù)先設(shè)定數(shù)量的工作線程,用于為進(jìn)入的block服務(wù)?,F(xiàn)在為這個(gè)隊(duì)列做基礎(chǔ)的實(shí)現(xiàn)。
隊(duì)列實(shí)現(xiàn)
像線程池一樣,隊(duì)列將使用鎖來(lái)保護(hù)他的內(nèi)容。和線程池不一樣的地方是,他不需要做任何等待或者發(fā)信號(hào)的動(dòng)作,只是基本的互斥,所以我們使用普通的NSLock:
NSLock *_lock;
像線程池一樣,他維護(hù)一個(gè)掛起的block的隊(duì)列,使用NSMutableArray:
NSMutableArray *_pendingBlocks;
隊(duì)列需要知道這是串行的還是并發(fā)的:
BOOL _serial;
當(dāng)這個(gè)值為真,它還需要跟蹤是否有一個(gè)block在線程池中運(yùn)行:
BOOL _serialRunning;
并發(fā)隊(duì)列無(wú)論是否有任務(wù)在運(yùn)行都表現(xiàn)的一樣,所以不跟蹤這些。
全局隊(duì)列作為一個(gè)全局變量來(lái)存儲(chǔ),底層共享的線程池也是。他們都在+initialize方法中創(chuàng)建:
static MADispatchQueue *gGlobalQueue;
static MAThreadPool *gThreadPool;
+ (void)initialize {
if(self == [MADispatchQueue class]) {
gGlobalQueue = [[MADispatchQueue alloc] initSerial: NO];
gThreadPool = [[MAThreadPool alloc] init];
}
}
獲取全局隊(duì)列方法只是返回這個(gè)變量,因?yàn)閕nitialize方法中確保已經(jīng)創(chuàng)建了他:
+ (MADispatchQueue *)globalQueue {
return gGlobalQueue;
}
初始化隊(duì)列由分配鎖,掛起block隊(duì)列以及設(shè)置_serial變量這些工作組成:
- (id)initSerial: (BOOL)serial {
if ((self = [super init])) {
_lock = [[NSLock alloc] init];
_pendingBlocks = [[NSMutableArray alloc] init];
_serial = serial;
}
return self;
}
在我們接觸剩余的公開(kāi)API之前,有一個(gè)底層的方法需要?jiǎng)?chuàng)建,這個(gè)方法將在線程池派發(fā)一個(gè)單一的block,然后調(diào)用他自己來(lái)運(yùn)行另一個(gè)block:
- (void)dispatchOneBlock {
這個(gè)方法的目的是在線程池運(yùn)行東西,所以他在這里派發(fā):
[gThreadPool addBlock: ^{
然后他抓住了隊(duì)列里的第一個(gè)block。自然的,這必須結(jié)合鎖來(lái)完成,以避免災(zāi)難事故:
[_lock lock];
dispatch_block_t block = [_pendingBlocks firstObject];
[_pendingBlocks removeObjectAtIndex: 0];
[_lock unlock];
隨著獲得block以及釋放鎖,block能夠安全得在后臺(tái)線程執(zhí)行
block();
如果隊(duì)列是并發(fā)的,那么這就是所有要做的。如果這是串行的,還需要:
if(_serial) {
在一個(gè)串行隊(duì)列,將建立額外的block,但是不能在block完成之前喚起。當(dāng)一個(gè)block完成, dispatchOneBlock會(huì)查看隊(duì)列中是否有其他掛起的block,如果有,他會(huì)調(diào)用自己去派發(fā)下一個(gè)block,如果沒(méi)有,他會(huì)將隊(duì)列的運(yùn)行狀態(tài)設(shè)回NO:
[_lock lock];
if([_pendingBlocks count] > 0) {
[self dispatchOneBlock];
} else {
_serialRunning = NO;
}
[_lock unlock];
}
}];
}
用這個(gè)方法來(lái)實(shí)現(xiàn)dispatchAsync是相當(dāng)簡(jiǎn)單的。添加block到掛起的block的隊(duì)列,設(shè)置狀態(tài)并且視情況喚起dispatchOneBlock:
- (void)dispatchAsync: (dispatch_block_t)block {
[_lock lock];
[_pendingBlocks addObject: block];
如果一個(gè)串行隊(duì)列是閑置狀態(tài),那么設(shè)置為運(yùn)行狀態(tài)并調(diào)用dispatchOneBlock來(lái)執(zhí)行要做的事:
if(_serial && !_serialRunning) {
_serialRunning = YES;
[self dispatchOneBlock];
如果隊(duì)列是并發(fā)的,那么無(wú)條件的調(diào)用dispatchOneBlock。這能確保新的block能夠盡可能快的執(zhí)行,盡管另一個(gè)block已經(jīng)在運(yùn)行中,因?yàn)樵诓l(fā)的情況下允許多個(gè)blocks執(zhí)行:
} else if (!_serial) {
[self dispatchOneBlock];
}
如果一個(gè)串行已經(jīng)運(yùn)行,那沒(méi)什么更多要做的了。dispatchOneBlock會(huì)執(zhí)行完所有添加到隊(duì)列的block?,F(xiàn)在釋放鎖:
[_lock unlock];
}
在dispatchSync方面,GCD當(dāng)停止隊(duì)列里其他block時(shí),在調(diào)用的線程上直接運(yùn)行block(如果這是串行)。我們不想嘗試做到這么智能。取而代之的,我們只是包裝一下dispatchAsync:,使他能夠等待完成執(zhí)行。
他使用一個(gè)局部NSCondition變量,附加一個(gè)done的BOOL變量來(lái)表明什么時(shí)候block已經(jīng)完成:
- (void)dispatchSync: (dispatch_block_t)block {
NSCondition *condition = [[NSCondition alloc] init];
__block BOOL done = NO;
然后他異步的派發(fā)block。這里調(diào)用的是傳入的block,然后設(shè)置狀態(tài)為完成并且讓條件鎖發(fā)送信號(hào):
[self dispatchAsync: ^{
block();
[condition lock];
done = YES;
[condition signal];
[condition unlock];
}];
回到原來(lái)正在調(diào)用的線程,我們要做的是等待狀態(tài)被設(shè)為done,然后返回。
[condition lock];
while (!done) {
[condition wait];
}
[condition unlock];
}
到此,block的執(zhí)行已經(jīng)完成了,這也是MADispatchQueue的API最后一點(diǎn)要做的了。
結(jié)論
一個(gè)全局的線程池能夠通過(guò)一組工作的block和一些比較智能的線程來(lái)實(shí)現(xiàn)。使用一個(gè)共享的全局線程池,能夠創(chuàng)建一個(gè)提供串行/并發(fā)和同步/異步派發(fā)的基礎(chǔ)派發(fā)隊(duì)列的API。本次重建缺少了許多GCD很棒的特性,并且非常低效。不管怎樣這讓我們很好的了解了內(nèi)部工作原理。