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)度,如圖所示:
上面的源碼我們看如下部分:
其中有一個(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ā)中一般有兩種用法:
- 單一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