避免使用 GCD Global隊(duì)列創(chuàng)建Runloop常駐線程
本文對應(yīng) Demo 以及 Markdown 文件在倉庫中,文中的錯(cuò)誤可以提 PR 到這個(gè)文件,我會(huì)及時(shí)更改。
目錄
GCD Global隊(duì)列創(chuàng)建線程進(jìn)行耗時(shí)操作的風(fēng)險(xiǎn)
先思考下如下幾個(gè)問題:
- 新建線程的方式有哪些?各自的優(yōu)缺點(diǎn)是什么?
- dispatch_async 函數(shù)分發(fā)到全局隊(duì)列一定會(huì)新建線程執(zhí)行任務(wù)么?
- 如果全局隊(duì)列對應(yīng)的線程池如果滿了,后續(xù)的派發(fā)的任務(wù)會(huì)怎么處置?有什么風(fēng)險(xiǎn)?
答案大致是這樣的:dispatch_async 函數(shù)分發(fā)到全局隊(duì)列不一定會(huì)新建線程執(zhí)行任務(wù),全局隊(duì)列底層有一個(gè)的線程池,如果線程池滿了,那么后續(xù)的任務(wù)會(huì)被 block 住,等待前面的任務(wù)執(zhí)行完成,才會(huì)繼續(xù)執(zhí)行。如果線程池中的線程長時(shí)間不結(jié)束,后續(xù)堆積的任務(wù)會(huì)越來越多,此時(shí)就會(huì)存在 APP crash的風(fēng)險(xiǎn)。
比如:
- (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 {
//模擬耗時(shí)操作,比如DB,網(wǎng)絡(luò),文件讀寫等等
sleep(30);
NSLog(@"----:%ld",index);
}
以上邏輯用真機(jī)測試會(huì)有卡死的幾率,并非每次都會(huì)發(fā)生,但多嘗試幾次就會(huì)復(fù)現(xiàn),伴隨前后臺切換,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
}
對于執(zhí)行的任務(wù)來說,所執(zhí)行的線程具體是哪個(gè)線程,則是通過 GCD 的線程池(Thread Pool)來進(jìn)行調(diào)度,正如Concurrent Programming: APIs and Challenges文章里給的示意圖所示:
上面貼的源碼,我們關(guān)注如下的部分:
其中有一個(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));
從源碼中我們可以對應(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ù)再提交 block 就不會(huì)再重新創(chuàng)建線程。這就是為什么 Demo 會(huì)造成卡頓甚至凍屏的原因。
避免使用 GCD Global 隊(duì)列創(chuàng)建 Runloop 常駐線程
在做網(wǎ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ā)中一般有兩種用法:
- 單一 Runloop 常駐線程:在 APP 的生命周期中開啟了唯一的常駐線程來進(jìn)行網(wǎng)絡(luò)請求,常用于網(wǎng)絡(luò)庫,或者有維持長連接需求的庫,比如: AFNetworking 、 SocketRocket。
- 多個(gè) Runloop 常駐線程:每進(jìn)行一次網(wǎng)絡(luò)請求就開啟一條 Runloop 常駐線程,這條線程的生命周期的起點(diǎn)是網(wǎng)絡(luò)請求開始,終點(diǎn)是網(wǎng)絡(luò)請求結(jié)束,或者網(wǎng)絡(luò)請求超時(shí)。
單一 Runloop 常駐線程
先說第一種用法:
以 AFNetworking 為例,AFURLConnectionOperation 這個(gè)類是基于 NSURLConnection 構(gòu)建的,其希望能在后臺線程接收 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è)小 Demo 來模擬這種場景,
我們模擬了一個(gè)場景:假設(shè)所有的網(wǎng)絡(luò)請求全部超時(shí),或者服務(wù)端根本不響應(yīng),然后網(wǎng)絡(luò)庫超時(shí)檢測機(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ī)測試會(huì)有卡死的幾率,并非每次都會(huì)發(fā)生,但多嘗試幾次就會(huì)復(fù)現(xiàn),伴隨前后臺切換,crash幾率增大。
其中我們采用了 GCD 全局隊(duì)列的方式來創(chuàng)建常駐線程,因?yàn)樵趧?chuàng)建時(shí)可能已經(jīng)出現(xiàn)了全局隊(duì)列的線程池滿了的情況,所以 GCD 派發(fā)的任務(wù),無法執(zhí)行,而且我們把超時(shí)檢測的邏輯放進(jìn)了這個(gè)任務(wù)中,所以導(dǎo)致的情況就是,有很多任務(wù)的超時(shí)檢測功能失效了。此時(shí)就只能依賴于服務(wù)端響應(yīng)來結(jié)束該任務(wù)(服務(wù)端響應(yīng)能結(jié)束該任務(wù)的邏輯在 Demo 中未給出),但是如果再加之服務(wù)端不響應(yīng),那么任務(wù)就永遠(yuǎn)不會(huì)結(jié)束。后續(xù)的網(wǎng)絡(luò)請求也會(huì)就此 block 住,造成 crash。
如果我們把 GCD 全局隊(duì)列換成 NSThread 的方式,那么就可以保證每次都會(huì)創(chuàng)建新的線程。
注意:文章中只演示的是超時(shí) cancel runloop 的操作,實(shí)際項(xiàng)目中一定有其他主動(dòng) cancel runloop 的操作,就比如網(wǎng)絡(luò)請求成功或失敗后需要進(jìn)行cancel操作。代碼中沒有展示網(wǎng)絡(luò)請求成功或失敗后的 cancel 操作。
Demo 的這種模擬可能比較極端,但是如果你維護(hù)的是一個(gè)像 AFNetworking 這樣的一個(gè)網(wǎng)絡(luò)庫,你會(huì)放心把創(chuàng)建常駐線程這樣的操作交給 GCD 全局隊(duì)列嗎?因?yàn)檎麄€(gè) APP 是在共享一個(gè)全局隊(duì)列的線程池,那么如果 APP 把線程池沾滿了,甚至線程池長時(shí)間占滿且不結(jié)束,那么 AFNetworking 就自然不能再執(zhí)行任務(wù)了,所以我們看到,即使是只會(huì)創(chuàng)建一條常駐線程, AFNetworking 依然采用了 NSThread 的方式而非 GCD 全局隊(duì)列這種方式。
注釋:以下方法存在于老版本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;
}
正如你所看到的,沒有任何一個(gè)庫會(huì)用 GCD 全局隊(duì)列來創(chuàng)建常駐線程,而你也應(yīng)該
避免使用 GCD Global 隊(duì)列來創(chuàng)建 Runloop 常駐線程。