iOS多线程编程之GCD详解(二)

前言

上一篇详细介绍了介绍了GCD中的常用API,
考虑到篇幅问题,这里继续介绍另外的两个API。

Dispatch Semaphore 信号量

dispatch_semaphore_t 信号量本质上是一种锁。
关于iOS中各种锁和性能比较可以看下yykit作者的这篇博文,戳这里
不再安全的 OSSpinLock

下面我们看下信号量的使用:
dispatch_semaphore_t 的作用之一解决资源抢夺问题
之前提过,对于数据存储类似数据库,非原子性可变字典和可变数组等多线程下不安全的操作,可以使用同步队列保证线程安全,那么在并发队列中,可以使用信号量来解决资源抢夺问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建一个信号量,初始值为1
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1) ;
//创建可变数组
NSMutableArray *array = [[NSMutableArray alloc] init];
for(int i = 0; i< 1000; ++i) {
dispatch_async(queue, ^{

//这里会一直等待,直到信号量大于等于1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) ;

//执行到这里,消费一个信号量
NSLog(@"%@",[NSThread currentThread]);
[array addObject:[NSNumber numberWithInt:i]];

//这里增加一个信号量
dispatch_semaphore_signal(semaphore);
});
}

代码解读一下:
dispatch_semaphore_create(1) 创建了值为1信号量
dispatch_semaphore_wait ,如果信号量的值大于等于1,那么,信号量值减1,然后向下执行,如果信号量值为0,一直等待。直到大于等于1的时候,率先进入等待状态的异步队列率先执行
dispatch_semaphore_signal 信号量值加1

形象比喻一下:
一群人排队去银行办业务,银行初始只有一个窗口,第一个人办业务的时候,可用窗口就变成0个了,这个人办完业务,可用窗口加1,就变成1个了。

实际这种效果和加锁的本质一致。
dispatch_semaphore_t 的另外一个作用就是可以控制线程并发数量,之前我们提过,iOS7之后系统自动开辟的线程数量可以多达60-70,而GCD中并没有提供控制线程数量的API,NSOperation中可以设置最大线程数。

下面我们使用信号量来实现一下线程数量控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//线程并发数限制
static dispatch_semaphore_t limitSemaphore;
//控制专用队列
static dispatch_queue_t serialQueue;

//单例创建
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//设置最大线程并发数为5
limitCount = dispatch_semaphore_create(5);
serialQueue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
});

dispatch_async(serialQueue, ^{
//信号量>=1继续执行,否则等待
dispatch_semaphore_wait(limitSemaphore, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
//这里执行一些任务
NSLog(@"%@",[NSThread currentThread]);
//在该工作线程执行完成后释放信号量
dispatch_semaphore_signal(limitSemaphore);
});
});

dispatch source

dispatch source 是一组不常用的GCD API。是BSD系内核惯有功能kqueue的包装。kqueue的介绍可以看下这个kqueue wikipedia
简单来说,dispatch source是一个监视某些类型事件的对象。它支持所有kqueue所支持的事件以及mach(mach介绍可以看这里mach wikipedia))端口、内建计时器支持和用户事件,CPU负荷占用小,资源占用小。

dispatch source联结

联结的流程:在任一线程上调用dispatch_source_merge_data 这个函数后,会执行 Dispatch Source 事先定义好的句柄(可以简单理解句柄就是block )(是不是有点通知,回调的味道哈)
下面直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//全局队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建source
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
//定义source的句柄
dispatch_source_set_event_handler(source, ^{
//调用一次dispatch_source_merge_data会调用这个句柄
NSLog(@"%lu",dispatch_source_get_data(source));
});
//默认source是suspend的,需要resume生效
dispatch_resume(source);

//遍历10次
dispatch_apply(10, globalQueue, ^(size_t index) {
// merge data
dispatch_source_merge_data(source, 1);
});

这段程序简单逻辑:调用dispatch_source_merge_data 会触发实现定义好的事件

1
2
3
4
dispatch_source_set_event_handler(source, ^{
//调用一次dispatch_source_merge_data会调用这个句柄
NSLog(@"%lu",dispatch_source_get_data(source));
});

dispatch_source_create 函数参数

DISPATCH_SOURCE_TYPE_DATA_ADD 累加
当注册系统事件的时候,有时候系统还没来得及通知应用程序,这个时候,系统会累计传递过来的值

DISPATCH_SOURCE_TYPE_DATA_OR 逻辑或处理累计传递过来的值

其他:

DISPATCH_SOURCE_TYPE_MACH_SENDMACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收
DISPATCH_SOURCE_TYPE_PROC 监测进程相关事件
DISPATCH_SOURCE_TYPE_READ 可读取文件映像
DISPATCH_SOURCE_TYPE_SIGNAL 接收信号
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件系统变更
DISPATCH_SOURCE_TYPE_WRITE 可写入文件映像

注册事件处理程序通知,如果系统没来的及通知应用程序时候事件发生多次,这些事件会合并为一个事件(是不是类似于TCP协议中的nagle算法)。iOS开发者通常不会用到这种功能。但对于底层,这种处理方式会很高效.

简单流程总结:创建一个源,自定义累计方式,可以是and也可以是Or,自定义源也需要一个队列用来处理响应块,可以是主队列,也可以是并发队列。

在同一时间,只有一个响应块被分派。处理方法没执行完毕,另一个事件发生,事件以指定方式(ADD或者OR)进行累积。通过合并,保证了在高负载下稳定执行。
累计值通过 dispatch_source_get_data 获取。每次响应执行事件,这个值会被重置
dispatch_source_merge_data 发送一个事件
默认创建出来的source是挂起状态的,需要调用dispatch_resume 才可生效

除了高效的自定义一个source处理自定事件之外,我们也可以使用dispatch_source 来定义一个定时器,iOS开发中常用的定时器有NSTimerCADisplayLink 两种
NSTimer 受到runloop的状态影响精度
CADisplayLink 则和屏幕刷新度帧数一致
dispatch_source 作为定时器精度很高,是系统级别的源

demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//定时器作为属性创建
self.timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//开始时间
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC);
//间隔时间
uint64_t interval = 2.0 * NSEC_PER_SEC;
//设置时间
dispatch_source_set_timer( self.timerSource start, interval, 0);

//设置回调
dispatch_source_set_event_handler( self.timerSource, ^{
//处理事件
});

//启动定时器
dispatch_resume( self.timerSource);

总结

本文主要介绍了

dispatch_semaphore_t ,本质是一种底层锁,性能较高,可以用来解决多线程资源竞争,控制线程并发数。

dispatch source 最大的优势是联结,通过合并事件的方式,高效的处理事件分派,可以自定义source用来处理高负载应用场景响应。

dispatch source 可以作为高精度,系统源层级的定时器,在需要高精度应用场景下可以选用这种更加接近底层的定时器。

写了两篇博客来详解了下GCD,主要是对自我基础的一个总结。之前已经有很多写的很好的GCD文章。关于信号量和dispatch_source还有兴趣深入了解的可以阅读下官方文档。
推荐下阅读猿神的博客
Parse源码浅析系列(一)—Parse的底层多线程处理思路:GCD高级用法

参考书籍:
Objective-C高级编程