多线程之GCD

本文详细介绍了GCD在iOS中的使用,主要包括以下几个方面

  • GCD 简介
  • GCD优点
  • GCD基础概念
  • GCD 的使用步骤
  • GCD 组合方式
  • GCD 常用方法总结

GCD 简介

Grand Central Dispatch(GCD) 是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。

GCD优点

  • GCD 可用于多核的并行运算
  • GCD 会自动利用更多的 CPU 内核(比如双核、四核)
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码

GCD基础概念

为了不重复编写我专门写了一篇介绍基础概念文章详细介绍了多线程编程中的一些概念,如同步/异步、串行/并行、队列/线程/任务、进程/线程等,请了解相关概念后继续阅读。

GCD 的使用步骤

GCD 的使用步骤其实很简单,只有两步。

  1. 创建一个队列(串行队列或并发队列)
  2. 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)

队列的创建方法

1
2
3
4
5
6
7
8
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
// 主队列的获取方法,串行
dispatch_queue_t queue = dispatch_get_main_queue();
// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

任务的创建方法

1
2
3
4
5
6
7
8
// 同步执行任务创建方法
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
// 这里放异步执行任务代码
});

GCD 组合方式

  • 异步 + 串行
  • 异步 + 并发
  • 同步 + 串行(很少使用、可能死锁)
  • 同步 + 并发(很少使用)

结论:同步执行没有开启新线程的能力, 所有的任务都只能在当前线程执行

异步执行有开启新线程的能力, 但是, 有开启新线程的能力, 也不一定会利用这种能力, 也就是说, 异步执行是否开启新线程, 需要具体问题具体分析

具体组合的实践代码我在另外一篇文章有详细介绍,本篇将这部分跳过。

GCD其他函数用法

dispatch_after

该函数用于任务延时执行,具体使用方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-(void)GCDDelay{
//主队列延时
dispatch_time_t when_main = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));
dispatch_after(when_main, dispatch_get_main_queue(), ^{
NSLog(@"main_%@",[NSThread currentThread]);
});
//全局队列延时
dispatch_time_t when_global = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC));
dispatch_after(when_global, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"global_%@",[NSThread currentThread]);
});
//自定义队列延时
dispatch_time_t when_custom = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(when_custom, self.serialQueue, ^{
NSLog(@"custom_%@",[NSThread currentThread]);
});
}

dispatch_once

保证函数在整个生命周期内只会执行一次,使用方法如下:

1
2
3
4
5
6
7
8
+ (id)sharedInstance {
static TestClass *sharedInstance;
static dispatch_once_t onceToken; // typedef long dispatch_once_t;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}

dispatch_group

dispatch_group_create

用于创建任务组

1
dispatch_group_t dispatch_group_create(void);

dispatch_group_async

把异步任务提交到指定任务组和指定下拿出队列执行

1
2
3
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
  • group ——对应的任务组,之后可以通过dispatch_group_wait或者dispatch_group_notify监听任务组内任务的执行情况
  • queue ——block任务执行的线程队列,任务组内不同任务的队列可以不同
  • block —— 执行任务的block

dispatch_group_enter

用于添加对应任务组中的未执行完毕的任务数,执行一次,未执行完毕的任务数加1,当未执行完毕任务数为0的时候,才会使dispatch_group_wait解除阻塞和dispatch_group_notify的block执行

1
void dispatch_group_enter(dispatch_group_t group);

dispatch_group_leave

用于减少任务组中的未执行完毕的任务数,执行一次,未执行完毕的任务数减1,dispatch_group_enterdispatch_group_leave要匹配,不然系统会认为group任务没有执行完毕

1
void dispatch_group_leave(dispatch_group_t group);

dispatch_group_wait

等待组任务完成,会阻塞当前线程,当任务组执行完毕时,才会解除阻塞当前线程

1
2
long dispatch_group_wait(dispatch_group_t group, 
dispatch_time_t timeout);
  • group ——需要等待的任务组
  • timeout ——等待的超时时间(即等多久),单位为dispatch_time_t。如果设置为DISPATCH_TIME_FOREVER,则会一直等待(阻塞当前线程),直到任务组执行完毕

dispatch_group_notify

待任务组执行完毕时调用,不会阻塞当前线程

1
2
3
void dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
  • group ——需要监听的任务组
  • queue ——block任务执行的线程队列,和之前group执行的线程队列无关
  • block ——任务组执行完毕时需要执行的任务block

以下代码简单演示group的使用方法,并测试group中嵌套异步代码存在的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"group one start");
dispatch_group_async(group, queue, ^{
dispatch_async(queue, ^{
sleep(1); //这里线程睡眠1秒钟,模拟异步请求
NSLog(@"group one finish");
});
});

dispatch_group_notify(group, queue, ^{
NSLog(@"group finished");
});

控制台输出

1
2
3
2016-09-25 09:28:28.716 group one start
2016-09-25 09:28:28.717 group finished
2016-09-25 09:28:29.717 group one finish

从打印结果可以看出,在group中嵌套了一个异步任务时,group并没有等待group内的异步任务执行完毕才进入dispatch_group_notify中,这是因为,在dispatch_group_async中又启了一个异步线程,而异步线程是直接返回的,所以group就认为是执行完毕了。

对于以上这种情形,解决方案是使用dispatch_group_enterdispatch_group_leave方法来告知group组内任务何时才是真正的结束。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"group one start");
dispatch_group_enter(group);
dispatch_async(queue, ^{
sleep(1); //这里线程睡眠1秒钟,模拟异步请求
NSLog(@"group one finish");
dispatch_group_leave(group);
});

dispatch_group_notify(group, queue, ^{
NSLog(@"group finished");
});

控制台输出

1
2
3
2016-09-25 09:34:07.672 group one start
2016-09-25 09:34:08.677 group one finish
2016-09-25 09:34:08.678 group finished

以上代码,通过dispatch_group_enter告知group,一个任务开始,未执行完毕任务数加1,在异步线程任务执行完毕时,通过dispatch_group_leave告知group,一个任务结束,未执行完毕任务数减1,当未执行完毕任务数为0的时候,这时group才认为组内任务都执行完毕了(这个和GCD的信号量的机制有些相似),这时候才会回调dispatch_group_notify中的block。

dispatch_barrier_async

栅栏函数,使用此方法创建的任务,会查找当前队列中有没有其他任务要执行,如果有,则等待已有任务执行完毕后再执行,同时,在此任务之后进入队列的任务,需要等待此任务执行完成后,才能执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-(void)GCDbarrier{
dispatch_async(self.concurrentQueue, ^{
NSLog(@"任务1");
});
dispatch_async(self.concurrentQueue, ^{
NSLog(@"任务2");
});

dispatch_barrier_async(self.concurrentQueue, ^{
NSLog(@"任务barrier");
});

// NSLog(@"big");
dispatch_async(self.concurrentQueue, ^{
NSLog(@"任务3");
});
// NSLog(@"apple");
dispatch_async(self.concurrentQueue, ^{
NSLog(@"任务4");
});
}

打印结果:

1
2
3
4
5
ThreadDemo[1833:678739] 任务2
ThreadDemo[1833:678740] 任务1
ThreadDemo[1833:678740] 任务barrier
ThreadDemo[1833:678740] 任务3
ThreadDemo[1833:678739] 任务4

dispatch_apply

该函数用于重复执行某个任务,如果任务队列是并行队列,重复执行的任务会并发执行,如果任务队列为串行队列,则任务会顺序执行

1
2
3
4
5
6
7
8
9
//串行队列
dispatch_apply(5, self.serialQueue, ^(size_t i) {
NSLog(@"第%@次_%@",@(i),[NSThread currentThread]);
});

//并行队列
dispatch_apply(5, self.concurrentQueue, ^(size_t i) {
NSLog(@"第%@次_%@",@(i),[NSThread currentThread]);
});

dispatch_semaphore

信号量主要有3个函数,分别是:

1
2
3
4
5
6
//创建信号量,参数:信号量的初值,如果小于0则会返回NULL
dispatch_semaphore_create(信号量值)
//等待降低信号量
dispatch_semaphore_wait(信号量,等待时间)
//提高信号量
dispatch_semaphore_signal(信号量)

正常的使用顺序是先降低然后再提高,这两个函数通常成对使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-(void)dispatchSignal{
//crate的value表示,最多几个资源可访问
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//任务1
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 1");
sleep(1);
NSLog(@"complete task 1");
dispatch_semaphore_signal(semaphore);
});<br>
//任务2
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 2");
sleep(1);
NSLog(@"complete task 2");
dispatch_semaphore_signal(semaphore);
});<br>
//任务3
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 3");
sleep(1);
NSLog(@"complete task 3");
dispatch_semaphore_signal(semaphore);
});
}

执行结果:

img  

总结:由于设定的信号值为2,先执行两个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过2。

dispatch_Source

Dispatch Source是GCD中的一种基本数据类型,从字面意思可称其为调度源,它用于处理特定的系统底层事件,即:当一些特定的系统底层事件发生时,调度源会捕捉到这些事件,然后可以做相应的逻辑处理。

Dispatch Source可用来监听以下几类事件:

  • Timer Dispatch Source:定时调度源。
  • Signal Dispatch Source:监听UNIX信号调度源,比如监听代表挂起指令的SIGSTOP信号。
  • Descriptor Dispatch Source:监听文件相关操作和Socket相关操作的调度源。
  • Process Dispatch Source:监听进程相关状态的调度源。
  • Mach port Dispatch Source:监听Mach相关事件的调度源。
  • Custom Dispatch Source:监听自定义事件的调度源。

使用Dispatch Source时,通常是先指定一个希望监听的系统事件类型,再指定一个捕获到事件后进行逻辑处理的闭包或者函数作为回调函数,然后再指定一个该回调函数执行的Dispatch Queue即可。当监听到指定的系统事件发生时,Dispatch Source会将已指定的回调函数作为一个任务放入指定的队列中执行,也就是说当监听到系统事件后就会触发一个任务,并自动将其加入队列执行。
这里与通常的手动添加任务的模式不同,一旦将Diaptach Source与Dispatch Queue关联后,只要监听到系统事件,Dispatch Source就会自动将任务(回调函数)添加到关联的队列中,直到我们调用函数取消监听。

为了保证监听到事件后回调函数能够都到执行,已关联的Dispatch Queue会被Diaptach Source强引用。有些时候回调函数执行的时间较长,在这段时间内Dispatch Source又监听到多个系统事件,理论上就会形成事件积压,但好在Dispatch Source有很好的机制解决这个问题,当有多个事件积压时会根据事件类型,将它们进行关联和结合,形成一个新的事件。

Dispatch Source涉及的方法开发中使用的场景比较稀少,本文只讲解如何用它来构造一个定时器。

定时器

使用定时器时需要调用 dispatch_source_set_timer函数来配置定时器,这个函数有四个参数:

  • source:待配置的定时器类型的 Dispatch Source
  • start:控制定时器第一次触发的时刻。参数类型是 dispatch_time_t,这是一个opaque类型,我们不能直接操作它。我们得需要 dispatch_time 和 dispatch_walltime 函数来创建它们。另外,常量 DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER 通常很有用。
  • interval:触发间隔
  • leeway:定时器进度,单位纳秒;如果设为0,系统只是最大程度满足精度需求。精度越高功耗越大。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 - (void)timerDispatchSource
{
dispatch_source_t timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0, 0, dispatch_get_global_queue(0, 0));

if (timerSource)
{
//系统默认的时间类型
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 0 * NSEC_PER_SEC);
//walltime时间类型
// dispatch_time_t startTime = dispatch_walltime(NULL, 0 * NSEC_PER_SEC);
NSString *desc = timerSource.description;
dispatch_source_set_timer(timerSource, startTime, 1 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timerSource, ^{
static NSInteger i = 0;
++i;
NSLog(@"Timer %@ Task: %ld",desc,i);
});
dispatch_source_set_cancel_handler(timerSource, ^{
// NSLog(@"Timer:%@ canceled",timerSource);
});
dispatch_resume(timerSource);
}

_myTimerSource = timerSource; ///< 必须要保存,除非在hander中引用timerSource,否则出了作用域,Timer就会被释放
}

NSTimer 与 GCD Timer比较

NSTimer

  • 依赖NSRunloop
  • 容易导致内存泄漏
  • NSTimer的创建与撤销必须在同一个线程操作、 performSelector的创建与撤销必须在同一个线程操作

GCD Timer

  • 可以被当做对象放入数组或字典中
  • GCD Timer必须强引用,否则出了栈就会失效,这种失效不会触发取消处理器
  • GCD Timer精度可控
  • 如果使用dispatch_walltime来设置定时器的起始时间,定时器默认使用walltime来触发定时器;如果使用dispatch_time来设置定时器的起始时间,定时器默认使用系统时钟来触发定时器,然而当计算机休眠时,系统时钟也是休眠的。对于时间间隔比较大的定时器,使用dispatch_walltime来设置定时器的起始时间

###