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

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

本文對應 Demo 以及 Markdown 文件在倉庫中丸相,文中的錯誤可以提 PR 到這個文件薪介,我會及時更改浦夷。

目錄


GCD Global隊列創(chuàng)建線程進行耗時操作的風險

先思考下如下幾個問題:

  • 新建線程的方式有哪些举娩?各自的優(yōu)缺點是什么映琳?
  • dispatch_async 函數(shù)分發(fā)到全局隊列一定會新建線程執(zhí)行任務么?
  • 如果全局隊列對應的線程池如果滿了,后續(xù)的派發(fā)的任務會怎么處置源梭?有什么風險仲闽?

答案大致是這樣的:dispatch_async 函數(shù)分發(fā)到全局隊列不一定會新建線程執(zhí)行任務验庙,全局隊列底層有一個的線程池搏恤,如果線程池滿了,那么后續(xù)的任務會被 block 住掂咒,等待前面的任務執(zhí)行完成挨摸,才會繼續(xù)執(zhí)行。如果線程池中的線程長時間不結(jié)束,后續(xù)堆積的任務會越來越多,此時就會存在 APP crash的風險。

比如:

- (void)dispatchTest1 {
    for (NSInteger i = 0; i< 10000 ; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self dispatchTask:i];
        });
    }
}

- (void)dispatchTask:(NSInteger)index {
        //模擬耗時操作,比如DB,網(wǎng)絡(luò),文件讀寫等等
        sleep(30);
        NSLog(@"----:%ld",index);
}

以上邏輯用真機測試會有卡死的幾率布卡,并非每次都會發(fā)生崔挖,但多嘗試幾次就會復現(xiàn)卷哩,伴隨前后臺切換,crash幾率增大。

下面做一下分析:

參看 GCD 源碼我們可以看到全局隊列的相關(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
}

對于執(zhí)行的任務來說瓦堵,所執(zhí)行的線程具體是哪個線程基协,則是通過 GCD 的線程池(Thread Pool)來進行調(diào)度澜驮,正如Concurrent Programming: APIs and Challenges文章里給的示意圖所示:

上面貼的源碼卦绣,我們關(guān)注如下的部分:

其中有一個用來記錄線程池大小的字段 dgq_thread_pool_size八堡。這個字段標記著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));

從源碼中我們可以對應到官方文檔 :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.

也就是說:

全局隊列的底層是一個線程池汰现,向全局隊列中提交的 block口叙,都會被放到這個線程池中執(zhí)行,如果線程池已滿嗅战,后續(xù)再提交 block 就不會再重新創(chuàng)建線程妄田。這就是為什么 Demo 會造成卡頓甚至凍屏的原因。

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

在做網(wǎng)路請求時我們常常創(chuàng)建一個 Runloop 常駐線程用來接收驮捍、響應后續(xù)的服務端回執(zhí)疟呐,比如NSURLConnection、AFNetworking等等东且,我們可以稱這種線程為 Runloop 常駐線程启具。

正如上文所述,用 GCD Global 隊列創(chuàng)建線程進行耗時操作是存在風險的珊泳。那么我們可以試想下鲁冯,如果這個耗時操作變成了 runloop 常駐線程,會是什么結(jié)果色查?下面做一下分析:

先介紹下 Runloop 常駐線程的原理薯演,在開發(fā)中一般有兩種用法:

  • 單一 Runloop 常駐線程:在 APP 的生命周期中開啟了唯一的常駐線程來進行網(wǎng)絡(luò)請求,常用于網(wǎng)絡(luò)庫秧了,或者有維持長連接需求的庫跨扮,比如: AFNetworking 、 SocketRocket示惊。
  • 多個 Runloop 常駐線程:每進行一次網(wǎng)絡(luò)請求就開啟一條 Runloop 常駐線程好港,這條線程的生命周期的起點是網(wǎng)絡(luò)請求開始愉镰,終點是網(wǎng)絡(luò)請求結(jié)束米罚,或者網(wǎng)絡(luò)請求超時。

單一 Runloop 常駐線程

先說第一種用法:

以 AFNetworking 為例丈探,AFURLConnectionOperation 這個類是基于 NSURLConnection 構(gòu)建的录择,其希望能在后臺線程接收 Delegate 回調(diào)。為此 AFNetworking 單獨創(chuàng)建了一個線程,并在這個線程中啟動了一個 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;
}

多個 Runloop 常駐線程

第二種用法隘竭,我寫了一個小 Demo 來模擬這種場景塘秦,

我們模擬了一個場景:假設(shè)所有的網(wǎng)絡(luò)請求全部超時,或者服務端根本不響應动看,然后網(wǎng)絡(luò)庫超時檢測機制的做法:

#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];//在實際開發(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__), @"");
        });
    }

以上邏輯用真機測試會有卡死的幾率菱皆,并非每次都會發(fā)生须误,但多嘗試幾次就會復現(xiàn),伴隨前后臺切換仇轻,crash幾率增大京痢。

其中我們采用了 GCD 全局隊列的方式來創(chuàng)建常駐線程,因為在創(chuàng)建時可能已經(jīng)出現(xiàn)了全局隊列的線程池滿了的情況篷店,所以 GCD 派發(fā)的任務祭椰,無法執(zhí)行,而且我們把超時檢測的邏輯放進了這個任務中疲陕,所以導致的情況就是方淤,有很多任務的超時檢測功能失效了。此時就只能依賴于服務端響應來結(jié)束該任務(服務端響應能結(jié)束該任務的邏輯在 Demo 中未給出)鸭轮,但是如果再加之服務端不響應臣淤,那么任務就永遠不會結(jié)束。后續(xù)的網(wǎng)絡(luò)請求也會就此 block 住窃爷,造成 crash邑蒋。

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

注意:文章中只演示的是超時 cancel runloop 的操作医吊,實際項目中一定有其他主動 cancel runloop 的操作,就比如網(wǎng)絡(luò)請求成功或失敗后需要進行cancel操作逮京。代碼中沒有展示網(wǎng)絡(luò)請求成功或失敗后的 cancel 操作卿堂。

Demo 的這種模擬可能比較極端,但是如果你維護的是一個像 AFNetworking 這樣的一個網(wǎng)絡(luò)庫懒棉,你會放心把創(chuàng)建常駐線程這樣的操作交給 GCD 全局隊列嗎草描?因為整個 APP 是在共享一個全局隊列的線程池,那么如果 APP 把線程池沾滿了策严,甚至線程池長時間占滿且不結(jié)束穗慕,那么 AFNetworking 就自然不能再執(zhí)行任務了,所以我們看到妻导,即使是只會創(chuàng)建一條常駐線程逛绵, AFNetworking 依然采用了 NSThread 的方式而非 GCD 全局隊列這種方式怀各。

注釋:以下方法存在于老版本AFN 2.x 中。

+ (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;
}

正如你所看到的术浪,沒有任何一個庫會用 GCD 全局隊列來創(chuàng)建常駐線程瓢对,而你也應該

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末胰苏,一起剝皮案震驚了整個濱河市硕蛹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌硕并,老刑警劉巖妓美,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鲤孵,居然都是意外死亡壶栋,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門普监,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贵试,“玉大人,你說我怎么就攤上這事凯正”胁#” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵廊散,是天一觀的道長桑滩。 經(jīng)常有香客問我,道長允睹,這世上最難降的妖魔是什么运准? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮缭受,結(jié)果婚禮上胁澳,老公的妹妹穿的比我還像新娘。我一直安慰自己米者,他們只是感情好韭畸,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蔓搞,像睡著了一般胰丁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上喂分,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天锦庸,我揣著相機與錄音,去河邊找鬼妻顶。 笑死酸员,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的讳嘱。 我是一名探鬼主播幔嗦,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼沥潭!你這毒婦竟也來了邀泉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤钝鸽,失蹤者是張志新(化名)和其女友劉穎汇恤,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拔恰,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡因谎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了颜懊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片财岔。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖河爹,靈堂內(nèi)的尸體忽然破棺而出匠璧,到底是詐尸還是另有隱情,我是刑警寧澤咸这,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布夷恍,位于F島的核電站,受9級特大地震影響媳维,放射性物質(zhì)發(fā)生泄漏酿雪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一侄刽、第九天 我趴在偏房一處隱蔽的房頂上張望执虹。 院中可真熱鬧,春花似錦唠梨、人聲如沸袋励。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茬故。三九已至,卻和暖如春蚁鳖,著一層夾襖步出監(jiān)牢的瞬間磺芭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工醉箕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留钾腺,地道東北人徙垫。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像放棒,于是被迫代替她去往敵國和親姻报。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

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