避免使用GCD Global隊(duì)列創(chuàng)建Runloop常駐線程

GCD Global隊(duì)列創(chuàng)建線程進(jìn)行耗時(shí)操作的風(fēng)險(xiǎn)

先思考如下幾個(gè)問題:

  • 新建線程的方式有那些?各自的優(yōu)缺點(diǎn)是什么贺奠?
  • dispatch_async函數(shù)分發(fā)到全局隊(duì)列一定會(huì)新建線程嗎承粤?
  • 如果全局隊(duì)列對(duì)應(yīng)的線程池滿了沧奴,后續(xù)的派發(fā)任務(wù)會(huì)怎么處置昭灵?有什么風(fēng)險(xiǎn)店展?
    答案大概是這樣的:dispatch_async函數(shù)分發(fā)到全局隊(duì)列不一定會(huì)新建線程執(zhí)行任務(wù)苛聘,全局隊(duì)列底層有一個(gè)線程池涂炎,如果創(chuàng)建滿了,那么后續(xù)的任務(wù)會(huì)被block住设哗,等待前面任務(wù)執(zhí)行完成唱捣,才會(huì)繼續(xù)執(zhí)行。如果線程池中的線程長(zhǎng)時(shí)間不結(jié)束网梢,后續(xù)堆積的任務(wù)會(huì)越來越多震缭,此時(shí)就會(huì)存在crash的風(fēng)險(xiǎn)
    比如下面代碼:
- (void)gcdGlobal {
    for (int i = 0; i < 10000; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self doSomething:i];
        });
    }
}
- (void)doSomething:(int)task {
    //模擬耗時(shí)操作,比如網(wǎng)絡(luò)請(qǐng)求
    sleep(30);
    NSLog(@"----:%d",task);
    
}

以上邏輯用真機(jī)測(cè)試會(huì)有卡死的幾率战虏,并非每次都會(huì)發(fā)生拣宰,但多嘗試幾次就會(huì)復(fù)現(xiàn),前后臺(tái)切換烦感,crash幾率增大巡社。
分析如下:
參看 GCD 源碼我們可以看到全局隊(duì)列的相關(guān)源碼如下:

DISPATCH_NOINLINE
static void
_dispatch_queue_wakeup_global_slow(dispatch_queue_t dq, unsigned int n)
{
   dispatch_root_queue_context_t qc = dq->do_ctxt;
   uint32_t i = n;
   int r;

   _dispatch_debug_root_queue(dq, __func__);
   dispatch_once_f(&_dispatch_root_queues_pred, NULL,
           _dispatch_root_queues_init);

#if HAVE_PTHREAD_WORKQUEUES
#if DISPATCH_USE_PTHREAD_POOL
   if (qc->dgq_kworkqueue != (void*)(~0ul))
#endif
   {
       _dispatch_root_queue_debug("requesting new worker thread for global "
               "queue: %p", dq);
#if DISPATCH_USE_LEGACY_WORKQUEUE_FALLBACK
       if (qc->dgq_kworkqueue) {
           pthread_workitem_handle_t wh;
           unsigned int gen_cnt;
           do {
               r = pthread_workqueue_additem_np(qc->dgq_kworkqueue,
                       _dispatch_worker_thread4, dq, &wh, &gen_cnt);
               (void)dispatch_assume_zero(r);
           } while (--i);
           return;
       }
#endif // DISPATCH_USE_LEGACY_WORKQUEUE_FALLBACK
#if HAVE_PTHREAD_WORKQUEUE_SETDISPATCH_NP
       if (!dq->dq_priority) {
           r = pthread_workqueue_addthreads_np(qc->dgq_wq_priority,
                   qc->dgq_wq_options, (int)i);
           (void)dispatch_assume_zero(r);
           return;
       }
#endif
#if HAVE_PTHREAD_WORKQUEUE_QOS
       r = _pthread_workqueue_addthreads((int)i, dq->dq_priority);
       (void)dispatch_assume_zero(r);
#endif
       return;
   }
#endif // HAVE_PTHREAD_WORKQUEUES
#if DISPATCH_USE_PTHREAD_POOL
   dispatch_pthread_root_queue_context_t pqc = qc->dgq_ctxt;
   if (fastpath(pqc->dpq_thread_mediator.do_vtable)) {
       while (dispatch_semaphore_signal(&pqc->dpq_thread_mediator)) {
           if (!--i) {
               return;
           }
       }
   }
   uint32_t j, t_count;
   // seq_cst with atomic store to tail <rdar://problem/16932833>
   t_count = dispatch_atomic_load2o(qc, dgq_thread_pool_size, seq_cst);
   do {
       if (!t_count) {
           _dispatch_root_queue_debug("pthread pool is full for root queue: "
                   "%p", dq);
           return;
       }
       j = i > t_count ? t_count : i;
   } while (!dispatch_atomic_cmpxchgvw2o(qc, dgq_thread_pool_size, t_count,
           t_count - j, &t_count, acquire));

   pthread_attr_t *attr = &pqc->dpq_thread_attr;
   pthread_t tid, *pthr = &tid;
#if DISPATCH_ENABLE_PTHREAD_ROOT_QUEUES
   if (slowpath(dq == &_dispatch_mgr_root_queue)) {
       pthr = _dispatch_mgr_root_queue_init();
   }
#endif
   do {
       _dispatch_retain(dq);
       while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
           if (r != EAGAIN) {
               (void)dispatch_assume_zero(r);
           }
           _dispatch_temporary_resource_shortage();
       }
   } while (--j);
#endif // DISPATCH_USE_PTHREAD_POOL
}

對(duì)于執(zhí)行的任務(wù)來說,所執(zhí)行的線程具體是哪個(gè)線程手趣,則是通過 GCD 的線程池(Thread Pool)來進(jìn)行調(diào)度,如圖所示:


gcdPool.jpg

上面的源碼我們看如下部分:
其中有一個(gè)用來記錄線程池大小的字段 dgq_thread_pool_size 這個(gè)字段標(biāo)記著GCD線程池的大小晌该。摘錄上面源碼的一部分:

uint32_t j, t_count;
  // seq_cst with atomic store to tail <rdar://problem/16932833>
  t_count = dispatch_atomic_load2o(qc, dgq_thread_pool_size, seq_cst);
  do {
      if (!t_count) {
          _dispatch_root_queue_debug("pthread pool is full for root queue: "
                  "%p", dq);
          return;
      }
      j = i > t_count ? t_count : i;
  } while (!dispatch_atomic_cmpxchgvw2o(qc, dgq_thread_pool_size, t_count,
          t_count - j, &t_count, acquire));

從源碼中我們可以對(duì)應(yīng)到官方文檔 :Getting the Global Concurrent Dispatch Queues里的說法:

A concurrent dispatch queue is useful when you have multiple tasks that can run in parallel. A concurrent queue is still a queue in that it dequeues tasks in a first-in, first-out order; however, a concurrent queue may dequeue additional tasks before any previous tasks finish. The actual number of tasks executed by a concurrent queue at any given moment is variable and can change dynamically as conditions in your application change. Many factors affect the number of tasks executed by the concurrent queues, including the number of available cores, the amount of work being done by other processes, and the number and priority of tasks in other serial dispatch queues.
也就是說:
全局隊(duì)列的底層是一個(gè)線程池,向全局隊(duì)列提交的block绿渣,都會(huì)被放到這個(gè)線程池中執(zhí)行朝群,如果線程池已滿,后續(xù)的任務(wù)不會(huì)再重新創(chuàng)建線程中符。這就是為什么上文-(void)gcdGlobal這個(gè)方法會(huì)造成卡頓的原因姜胖。

避免使用 GCD Global 隊(duì)列創(chuàng)建 Runloop 常駐線程

在做網(wǎng)絡(luò)請(qǐng)求時(shí)我們常常創(chuàng)建一個(gè)runloop常駐線程來接收、響應(yīng)后續(xù)的服務(wù)端回執(zhí)淀散,比如NSURLConnection右莱、AFNetworking等等,我們可以稱這種線程為 Runloop 常駐線程吧凉。正如上文所述隧出,用 GCD Global 隊(duì)列創(chuàng)建線程進(jìn)行耗時(shí)操作是存在風(fēng)險(xiǎn)的。那么我們可以試想下阀捅,如果這個(gè)耗時(shí)操作變成了 runloop 常駐線程胀瞪,會(huì)是什么結(jié)果?下面做一下分析:

先介紹下 Runloop 常駐線程的原理,在開發(fā)中一般有兩種用法:
  1. 單一Runloop常駐線程:在 APP 的生命周期中開啟了唯一的常駐線程來進(jìn)行網(wǎng)絡(luò)請(qǐng)求凄诞,常用于網(wǎng)絡(luò)庫圆雁,或者有維持長(zhǎng)連接需求的庫,比如: AFNetworking 帆谍、 SocketRocket伪朽。
    2.多個(gè) Runloop 常駐線程:每進(jìn)行一次網(wǎng)絡(luò)請(qǐng)求就開啟一條 Runloop 常駐線程,這條線程的生命周期的起點(diǎn)是網(wǎng)絡(luò)請(qǐng)求開始汛蝙,終點(diǎn)是網(wǎng)絡(luò)請(qǐng)求結(jié)束烈涮,或者網(wǎng)絡(luò)請(qǐng)求超時(shí)。

單一 Runloop 常駐線程

以 AFNetworking 為例窖剑,AFURLConnectionOperation 這個(gè)類是基于 NSURLConnection 構(gòu)建的坚洽,其希望能在后臺(tái)線程接收 Delegate 回調(diào)。為此 AFNetworking 單獨(dú)創(chuàng)建了一個(gè)線程西土,并在這個(gè)線程中啟動(dòng)了一個(gè) RunLoop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

多個(gè) Runloop 常駐線程

我們模擬了一個(gè)場(chǎng)景:假設(shè)所有的網(wǎng)絡(luò)請(qǐng)求全部超時(shí)讶舰,或者服務(wù)端根本不響應(yīng),然后網(wǎng)絡(luò)庫超時(shí)檢測(cè)機(jī)制的做法:

#import "Foo.h"

@interface Foo()  {
    NSRunLoop *_runloop;
    NSTimer *_timeoutTimer;
    NSTimeInterval _timeoutInterval;
    dispatch_semaphore_t _sem;
}
@end

@implementation Foo

- (instancetype)init {
    if (!(self = [super init])) {
        return nil;
    }
    _timeoutInterval = 1 ;
    _sem = dispatch_semaphore_create(0);
    // Do any additional setup after loading the view, typically from a nib.
    return self;
}

- (id)test {
    // 第一種方式:
    // NSThread *networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint0:) object:nil];
    // [networkRequestThread start];
    //第二種方式:
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
        [self networkRequestThreadEntryPoint0:nil];
    });
    dispatch_semaphore_wait(_sem, DISPATCH_TIME_FOREVER);
    return @(YES);
}
- (void)networkRequestThreadEntryPoint0:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"CYLTest"];
        _runloop = [NSRunLoop currentRunLoop];
        [_runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        _timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(stopLoop) userInfo:nil repeats:NO];
        [_runloop addTimer:_timeoutTimer forMode:NSRunLoopCommonModes];
        [_runloop run];//在實(shí)際開發(fā)中最好使用這種方式來確保能runloop退出需了,做雙重的保障[runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(timeoutInterval+5)]];
    }
}
- (void)stopLoop {
    CFRunLoopStop([_runloop getCFRunLoop]);
    dispatch_semaphore_signal(_sem);
}
@end

如果

   for (int i = 0; i < 300 ; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
            [[Foo new] test];
            NSLog(@"??類名與方法名:%@(在第%@行)跳昼,描述:%@", @(__PRETTY_FUNCTION__), @(__LINE__), @"");
        });
    }

以上邏輯用真機(jī)測(cè)試會(huì)有卡死的幾率,并非每次都會(huì)發(fā)生肋乍,但多嘗試幾次就會(huì)復(fù)現(xiàn)鹅颊,伴隨前后臺(tái)切換,crash幾率增大住拭。
其中我們采用了 GCD 全局隊(duì)列的方式來創(chuàng)建常駐線程挪略,因?yàn)樵趧?chuàng)建時(shí)可能已經(jīng)出現(xiàn)了全局隊(duì)列的線程池滿了的情況,所以 GCD 派發(fā)的任務(wù)滔岳,無法執(zhí)行杠娱,而且我們把超時(shí)檢測(cè)的邏輯放進(jìn)了這個(gè)任務(wù)中,所以導(dǎo)致的情況就是谱煤,有很多任務(wù)的超時(shí)檢測(cè)功能失效了摊求。此時(shí)就只能依賴于服務(wù)端響應(yīng)來結(jié)束該任務(wù)(服務(wù)端響應(yīng)能結(jié)束該任務(wù)的邏輯在 Demo 中未給出),但是如果再加之服務(wù)端不響應(yīng)刘离,那么任務(wù)就永遠(yuǎn)不會(huì)結(jié)束室叉。后續(xù)的網(wǎng)絡(luò)請(qǐng)求也會(huì)就此 block 住,造成 crash硫惕。

如果我們把 GCD 全局隊(duì)列換成 NSThread 的方式茧痕,那么就可以保證每次都會(huì)創(chuàng)建新的線程。

注意:文章中只演示的是超時(shí) cancel runloop 的操作恼除,實(shí)際項(xiàng)目中一定有其他主動(dòng) cancel runloop 的操作踪旷,就比如網(wǎng)絡(luò)請(qǐng)求成功或失敗后需要進(jìn)行cancel操作曼氛。代碼中沒有展示網(wǎng)絡(luò)請(qǐng)求成功或失敗后的 cancel 操作。
Demo 的這種模擬可能比較極端令野,但是如果你維護(hù)的是一個(gè)像 AFNetworking 這樣的一個(gè)網(wǎng)絡(luò)庫舀患,你會(huì)放心把創(chuàng)建常駐線程這樣的操作交給 GCD 全局隊(duì)列嗎?因?yàn)檎麄€(gè) APP 是在共享一個(gè)全局隊(duì)列的線程池气破,那么如果 APP 把線程池沾滿了聊浅,甚至線程池長(zhǎng)時(shí)間占滿且不結(jié)束,那么 AFNetworking 就自然不能再執(zhí)行任務(wù)了现使,所以我們看到低匙,即使是只會(huì)創(chuàng)建一條常駐線程, AFNetworking 依然采用了 NSThread 的方式而非 GCD 全局隊(duì)列這種方式朴下。

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

正如你所看到的努咐,沒有任何一個(gè)庫會(huì)用 GCD 全局隊(duì)列來創(chuàng)建常駐線程,而你也應(yīng)該避免使用 GCD Global 隊(duì)列來創(chuàng)建 Runloop 常駐線程殴胧。
本文參考:http://www.reibang.com/p/7eaedfc8f8f6

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市佩迟,隨后出現(xiàn)的幾起案子团滥,更是在濱河造成了極大的恐慌,老刑警劉巖报强,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灸姊,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡秉溉,警方通過查閱死者的電腦和手機(jī)力惯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來召嘶,“玉大人父晶,你說我怎么就攤上這事∨” “怎么了甲喝?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)铛只。 經(jīng)常有香客問我埠胖,道長(zhǎng),這世上最難降的妖魔是什么淳玩? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任直撤,我火速辦了婚禮,結(jié)果婚禮上蜕着,老公的妹妹穿的比我還像新娘谋竖。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布圈盔。 她就那樣靜靜地躺著豹芯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪驱敲。 梳的紋絲不亂的頭發(fā)上铁蹈,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音众眨,去河邊找鬼握牧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛娩梨,可吹牛的內(nèi)容都是我干的沿腰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼狈定,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼颂龙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起纽什,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤措嵌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后芦缰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體企巢,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年让蕾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了浪规。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡探孝,死狀恐怖笋婿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情再姑,我是刑警寧澤萌抵,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站元镀,受9級(jí)特大地震影響绍填,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜栖疑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一讨永、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧遇革,春花似錦卿闹、人聲如沸揭糕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽著角。三九已至,卻和暖如春旋恼,著一層夾襖步出監(jiān)牢的瞬間吏口,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工冰更, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留产徊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓蜀细,卻偏偏與公主長(zhǎng)得像舟铜,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子奠衔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

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