多线程与GCD思考

最近在阅读『程序员的自我修养—链接、装载与库』一书,第一章就提到了多线程的相关知识。作为计算机里一个非常重要的概念,多线程在iOS开发中也有着非常重要的地位,而线程安全也是在开发中要十分注意的地方,稍不留心就会产生意想不到的bug。这篇文章主要记录在项目中使用GCD的相关思考。

试验

多线程涉及到了队列,异步/同步,串行/并行等概念,而当这些概念和GCD结合时,很容易搞不清楚会产生怎样的结果,这里就用实例来验证一下

  • 同步并发

执行结果如下,任务在当前线程中串行执行,不会开启新线程。

  • 异步并发

执行结果如下,开启了新线程,任务交替/同时执行的。

  • 同步串行

执行结果如下,不会开启新线程,任务串行执行

  • 异步串行

执行结果如下,会开启一条新线程,任务串行执行

  • 主线程同步主队列

执行结果是互相等待造成死锁。

  • 其他线程中调用同步主队列

    通过detachNewThread方法创建线程并在创建的线程中执行syncMain方法。执行结果如下,不会开启新线程,任务串行执行。

    在这种情况下不会阻塞的原因是因为syncMain被放到了其他线程中,而任务都被追加到了主队列中,任务会在主线程中执行。syncMain 任务在其他线程中执行添加任务1到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的任务1,等任务1执行完毕,再接着执行任务2任务3。所以这里不会阻塞线程。

  • 异步主队列

执行结果如下,没有开启新线程,任务串行执行

总结一下各种情况

并发队列 串行队列 主队列
同步 不会开启新线程,串行执行任务 不会开启新线程,串行执行任务 主线程调用:
造成死锁
异步 会开启新线程,并发执行任务 会开启新线程,串行执行任务 不会开启新线程,串行执行任务。

GCD的使用场景

GCD作为一种轻量级的多线程解决方案,在我们项目中的使用频率是相当高的。下面就列举一下

耗时操作

这是项目里最常见的应用场景,在网络请求或者有其他耗时操作的时候,为了避免阻塞主线程,我们把这些耗时操作放在子线程里处理,然后在主线程使用处理结果并更新UI。

1
2
3
4
5
6
 dispatch_async(dispatch_get_global_queue(0, 0), ^{
//耗时操作,如网络请求
//请求完成之后,回到主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
//更新UI操作
}

延时执行

在需要延时执行某个任务时,可以使用dispatch_after

1
2
3
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//延时执行任务
});

执行一次任务

这也是项目中最常用到的。在创建单例、或者有整个程序运行过程中只执行一次的代码时会用到 GCD 的 dispatch_once 函数。使用dispatch_once 函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once也可以保证线程安全。

1
2
3
4
5
6
- (void)once {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(默认是线程安全的)
});
}

时序管理

某个页面存在多个请求,任务X需要在这多个请求都结束后再进行。一般情况下为了省事可能就会把B请求放在A请求的成功回调中发起,但是这样做会有几个潜在的问题:

  • 请求过多时需要写多层嵌套
  • 如果某个请求失败了,就不会执行下去
  • 请求同步进行,在网络不好的情况下会造成长时间等待

这时候就可以考虑使用dispatch_group来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
//请求A,在A的完成回调中调用leave
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
//请求B,在B的完成回调中调用leave
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
//请求C,在C的完成回调中调用leave
dispatch_group_leave(group);

});
dispatch_group_notify(group, dispatch_get_main_queue, ^{
//网络请求完成,刷新界面
});

或者使用dispatch_barrier_async,也可以实现同样的效果

定时器

由于NSTimer对Target是强引用,使用不当很容易造成内存泄漏。此外,NSTimer 的运行依赖于 Runloop,在 Runloop 的一次循环中,NSTimer 也只会执行一次,这使得在 Runloop 负担比较重时,可能会跳过NSTimer的执行,因此,在用到定时器的地方,可以用 GCD 的 TimerSource代替,YYKit中也使用了该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (instancetype)initWithFireTime:(NSTimeInterval)start
interval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
repeats:(BOOL)repeats {
self = [super init];
_repeats = repeats;
_timeInterval = interval;
_valid = YES;
_target = aTarget;
_selector = aSelector;

__weak typeof (self) weakSelf = self;
_lock = dispatch_semaphore_create(1);
_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_source, dispatch_time(DISPATCH_TIME_NOW, (start * NSEC_PER_SEC)), (interval * NSEC_PER_SEC), 0);
dispatch_source_set_event_handler(_source, ^{
[self fire];
});
dispatch_resume(_source);
return self;
}

线程安全

一个非常经典的多线程问题—火车站卖票。

运行的结果为

可以看到剩余的票数是乱的。这种时候就可以考虑使用semaphore来对线程进行加锁操作。

可以看到使用semaphore加锁之后,结果就是正确的。

GCD的坑

GCD给我们的开发带来了非常多的便利,但如果使用不当也会造成很多问题,下面记录下GCD使用过程中容易产生问题的点。

  • dispatch_sync导致死锁

这个问题在上文中已经提到过。再比如下面这段代码也会造成死锁。

1
2
3
4
5
6
7
8
9
10
11
- (void)test1 {
dispatch_sync(dispatch_get_main_queue(), ^{
//造成死锁
[self test2];
});
}
- (void)test2 {
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"test2");
});
}

所以在使用dispatch_sync时一定要慎重。

  • dispatch_once_t必须是全局或static变量

    在实现单例时正确的写法如下:

    1
    2
    3
    4
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    //实现单例代码
    });

    如果dispatch_once_t变量不是static或者全局的,就无法保证它只有一份实例,很可能造成不好排查的bug。

  • dispatch_after是延迟提交,不是延迟运行

官方文档对dispatch_after的解释如下

Enqueue a block for execution at the specified time.

它是指在指定的延时后,把一段block加入到指定队列中,而不是立刻执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)dispatchAfterTest {
//创建串行队列
dispatch_queue_t queue = dispatch_queue_create("com.chendongnan.testQueue", DISPATCH_QUEUE_SERIAL);
//提交一个block
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:5];
NSLog(@"第一个block执行完毕");
});
//3 秒以后提交block
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), queue, ^{
NSLog(@"第二个block执行");
});
}

这段代码的执行后,先输出第一个block执行完毕, 再输出第二个block执行。所以在使用dispatch_after的时候要注意这点。

  • dispatch_barrier_async只对dispatch_queue_create(label, attr)创建的队列有效

dispatch_barrier_asyncdispatch_get_global_queue得到的全局队列是无效的。