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

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

先思考如下幾個問題:

  • 新建線程的方式有那些?各自的優(yōu)缺點是什么?
  • dispatch_async函數(shù)分發(fā)到全局隊列一定會新建線程嗎?
  • 如果全局隊列對應(yīng)的線程池滿了,后續(xù)的派發(fā)任務(wù)會怎么處置?有什么風險?
    答案大概是這樣的:dispatch_async函數(shù)分發(fā)到全局隊列不一定會新建線程執(zhí)行任務(wù),全局隊列底層有一個線程池,如果創(chuàng)建滿了,那么后續(xù)的任務(wù)會被block住,等待前面任務(wù)執(zhí)行完成,才會繼續(xù)執(zhí)行。如果線程池中的線程長時間不結(jié)束,后續(xù)堆積的任務(wù)會越來越多,此時就會存在crash的風險
    比如下面代碼:
- (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 {
    //模擬耗時操作,比如網(wǎng)絡(luò)請求
    sleep(30);
    NSLog(@"----:%d",task);
    
}

以上邏輯用真機測試會有卡死的幾率,并非每次都會發(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í)行的任務(wù)來說,所執(zhí)行的線程具體是哪個線程,則是通過 GCD 的線程池(Thread Pool)來進行調(diào)度,如圖所示:


gcdPool.jpg

上面的源碼我們看如下部分:
其中有一個用來記錄線程池大小的字段 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));

從源碼中我們可以對應(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.
也就是說:
全局隊列的底層是一個線程池,向全局隊列提交的block,都會被放到這個線程池中執(zhí)行,如果線程池已滿,后續(xù)的任務(wù)不會再重新創(chuàng)建線程。這就是為什么上文-(void)gcdGlobal這個方法會造成卡頓的原因。

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

在做網(wǎng)絡(luò)請求時我們常常創(chuàng)建一個runloop常駐線程來接收、響應(yīng)后續(xù)的服務(wù)端回執(zhí),比如NSURLConnection、AFNetworking等等,我們可以稱這種線程為 Runloop 常駐線程。正如上文所述,用 GCD Global 隊列創(chuàng)建線程進行耗時操作是存在風險的。那么我們可以試想下,如果這個耗時操作變成了 runloop 常駐線程,會是什么結(jié)果?下面做一下分析:

先介紹下 Runloop 常駐線程的原理,在開發(fā)中一般有兩種用法:
  1. 單一Runloop常駐線程:在 APP 的生命周期中開啟了唯一的常駐線程來進行網(wǎng)絡(luò)請求,常用于網(wǎng)絡(luò)庫,或者有維持長連接需求的庫,比如: AFNetworking 、 SocketRocket。
    2.多個 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 常駐線程

我們模擬了一個場景:假設(shè)所有的網(wǎng)絡(luò)請求全部超時,或者服務(wù)端根本不響應(yīng),然后網(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ā)的任務(wù),無法執(zhí)行,而且我們把超時檢測的邏輯放進了這個任務(wù)中,所以導致的情況就是,有很多任務(wù)的超時檢測功能失效了。此時就只能依賴于服務(wù)端響應(yīng)來結(jié)束該任務(wù)(服務(wù)端響應(yīng)能結(jié)束該任務(wù)的邏輯在 Demo 中未給出),但是如果再加之服務(wù)端不響應(yīng),那么任務(wù)就永遠不會結(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í)行任務(wù)了,所以我們看到,即使是只會創(chuàng)建一條常駐線程, AFNetworking 依然采用了 NSThread 的方式而非 GCD 全局隊列這種方式。

+ (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)建常駐線程,而你也應(yīng)該避免使用 GCD Global 隊列來創(chuàng)建 Runloop 常駐線程。
本文參考:http://www.itdecent.cn/p/7eaedfc8f8f6

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

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

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