自己動(dòng)手創(chuàng)建Dispatch Queue

這篇文章來(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)部工作原理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文用來(lái)介紹 iOS 多線程中 GCD 的相關(guān)知識(shí)以及使用方法。這大概是史上最詳細(xì)、清晰的關(guān)于 GCD 的詳細(xì)講...
    花花世界的孤獨(dú)行者閱讀 575評(píng)論 0 1
  • 1.NSTimer不準(zhǔn)時(shí)的原因:(1).RunLoop循環(huán)處理時(shí)間,每次循環(huán)是固定時(shí)間,只有在這段時(shí)間才會(huì)去查看N...
    稻春閱讀 1,355評(píng)論 0 3
  • 原文鏈接深入理解GCD之dispatch_queue[https://www.neroxie.com/2019/0...
    NeroXie閱讀 9,538評(píng)論 8 37
  • NSThread 第一種:通過(guò)NSThread的對(duì)象方法 NSThread *thread = [[NSThrea...
    攻城獅GG閱讀 946評(píng)論 0 3
  • iOS的三種多線程技術(shù) 1.NSThread 每個(gè)NSThread對(duì)象對(duì)應(yīng)一個(gè)線程,量級(jí)較輕(真正的多線程) 2....
    XDUZ閱讀 716評(píng)論 0 3

友情鏈接更多精彩內(nèi)容