参考资料:《Objective-C 高级编程:iOS与OS X多线程和内存管理》
GCD(Grand Central Dispatch) 是异步执行任务的技术之一。是一种与 Block 有关的技术,它提供了对线程的抽象,这种抽象基于“派发队列”(Dispatch Queue)。开发者可将 Block 排入队列中,由 GCD 负责处理所有调度事宜。GCD 会根据系统资源情况,适时地创建、复用、摧毁后台线程,以便处理每个队列。
开发者只需要定义想执行的任务并追加到恰当的Dispatch Queue中,GCD 就能生成必要的线程并计划执行任务。
dispatch_async( queue, ^{
// 长时间处理任务
// ...
// 长时间处理结束,主线程使用该处理结果
dispatch_async( dispatch_get_main_queue(), ^{
// 主线程执行的处理
})
})
即执行处理的等待队列。开发者通过 dispatch_async 等API,在 Block 语法中记述想执行的处理并将其追加到 Dispatch Queue 中,Dispatch Queue 按照追加的顺序(FIFO)进行处理。
存在两种 Dispatch Queue:
Serial Dispatch Queue
串行队列只使用一个线程,会等待当前执行的任务结束后才会派发处理下一个任务,按照添加到队列的顺序进行派发。
Concurrent Dispatch Queue
并行队列同时使用多个线程,不等待当前执行的任务结束,如果当前的线程数足够,就会派发任务到多个线程同时执行。
并行执行的处理数量取决于当前系统的状态。由系统决定应当使用的线程数,并只生成所需的线程进行处理。当处理结束时,系统会结束不再需要的线程。
在并行队列中执行处理时,执行顺序会根据处理内容和系统状态发生改变,而串行队列中任务的执行顺序是固定的。
使用 dispatch_queue_create
生成派发队列。
dispatch_queue_t mySerialDispatchQueue =
dispatch_queue_create("com.example.gcd.mySerialDispatchQueue", NULL);
// 第一个参数为线程的名称,
// 第二参数为 NULL 时,生成 Serial Dispatch Queue;指定为 DISPATCH_QUEUE_CONCURRENT 时,生成 Concurrent Dispatch Queue。
用 dispatch_queue_create
函数可生成任意多个派发队列。
当生成多个 Serial Dispatch Queue
时,各个 Serial Dispatch Queue
将并行执行。系统对于一个 Serial Dispatch Queue
只生成并使用一个线程。
如果过多使用多线程,引起大量的上下文切换(CPU 寄存器等信息会保存到各自路径专用的内存块中),会消耗大量内存,大幅度降低系统的响应性能。
所以,只在为了避免资源竞争时使用 Serial Dispatch Queue
。
当想并行执行不会发生数据竞争等问题的处理时,使用 Concurrent Dispatch Queue
。对于 Concurrent Dispatch Queue
,不管生成多少,由于系统只使用有效管理的线程,因此不会发生 Serial Dispatch Queue
的那些问题。
Dispatch Queue 的内存管理类似 ARC,也通过引用计数进行管理,通过 dispatch_retain
函数和dispatch_release
函数来管理引用计数。
在 iOS 6.0 or Mac OS X 10.8 后,ARC 已经能够管理GCD 对象了。不再需要手动释放。
dispatch_queue_t myDispatchQueue =
dispatch_queue_create("com.example.gcd.myDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(myDispatchQueue, ^{NSLog(@" ")};);
也可以不用特意生成,使用系统提供的派发队列。
Main Dispatch Queue
:是在主线程中执行的Serial DIspatch Queue
,只有一个,添加到这个线程的任务都会在主线程的 RunLoop 中执行。
Global Dispatch Queue
:所有应用程序都能使用的 Concurrent Dispatch Queue
,因此没有必要通过 dispatch_queue_create
函数生成并行队列,只要获取 Global Dispatch Queue
使用即可。
Global Dispatch Queue
有 4 个执行优先级(High
, Default
, Low
, Background
),但此执行优先级只能进行大致的区分。
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
dispatch_queue_t globalDIspatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
// 第二个参数为保留字段
Dispatch Group
可以使 Concurrent Dispatch Queue
中的多个任务全部结束后在指定队列上执行 Block。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{NSLog(@"blk0");});
dispatch_group_async(group, queue, ^{NSLog(@"blk1");});
dispatch_group_async(group, queue, ^{NSLog(@"blk2");});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"done");
});
使用 DIspatch Group
都可以监视这些任务执行的结束。一旦检测到所有任务执行结束,就可将结束的处理异步派发到 指定队列 中。
可以使用 dispatch_group_wait
函数等待全部处理结束,该函数会堵塞当前线程,直到超过了指定的时间或全部任务执行结束。
long result = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// 若全部处理执行结束则返回 0;
// 若经过了指定时间但 group 中某一处理仍未结束,则返回值不为 0。
主要用于实现并行读串行写的功能,使用 Concurrent Dispatch Queue 和 dispatch_barrier_async 函数可实现高效率的数据库访问和文件访问。
使用:
dispatch_queue_t queue = dispatch_queue_create("com.barrierqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{NSLog(@"reading1");});
dispatch_async(queue, ^{NSLog(@"reading2");});
dispatch_async(queue, ^{NSLog(@"reading3");});
dispatch_barrier_async(queue, ^{NSLog(@"writing");});
dispatch_async(queue, ^{NSLog(@"reading4");});
dispatch_async(queue, ^{NSLog(@"reading5");});
dispatch_async(queue, ^{NSLog(@"reading6");});
用 dispatch_queue_create
生成 Concurrent Dispatch Queue
,然后用 dispatch_barrier_async
函数代替 dispatch_async
函数即可。
在 dispatch_get_global_queue 获取的队列上使用没有效果。
由 dispatch_barrier_async 添加的任务会等 Concurrent Dispatch Queue 上的并行执行的任务全部结束后,再将指定的任务追加到该 Concurrent Dispatch Queue 中。然后等待 dispatch_barrier_async 添加的任务执行完毕后,Concurrent Dispatch Queue 才恢复为并行执行。
dispatch_sync
会将指定 Block 同步添加到队列中,在添加的 Block 执行结束前,dispatch_sync
函数会一直堵塞直到执行结束。
而 dispatch_async
将任务添加到队列后会继续往下执行。
在同步队列中使用 dispatch_sync
添加任务会导致死锁:
dispatch_queue_t queue = dispatch_queue_create("queue", NULL);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{
NSLog(@"deadLock");
});
});
另外,dispatch_barrier_async 也有 sync 版本,功能类似,但会等待添加的 Block 执行结束再执行下面的代码。
使用 Dispatch Semaphore
可以进行更细粒度的排他控制。
DIspatch Semaphore 是持有计数的信号。如果信号量为 0 时则等待,信号量大于等于 1 则开始执行。
每当有线程进入“加锁代码”后就调用信号等待命令将计数减 1,此时计数为 0,其他线程无法进入,执行完后发送信号通知将信号量加 1,其他线程开始进入执行,如此一来就达到了线程同步目的。
通过 dispatch_semaphore_carete(1)
生成信号量为 1 的 DIspatch Semaphore
。
信号量的初始值,可以用来控制线程并发访问的最大数量。
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步。
dispatch_semaphore_wait
函数类似 dispatch_group_wait
,会堵塞线程等待,当计数值大于等于 1 时,对该计数值减 1 并从函数返回,继续执行后续代码。
dispatch_semaphore_signal(semaphore)
函数会将计数值加 1,如果有通过 dispatch_semaphore_wait
函数等待的线程,就由最先等待的线程执行。
可以通过 dispatch_semaphore_wait
的返回值进行分支处理:
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1null * NSEC_PER_SEC);
long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (result == 0) {
// 信号量计数值大于等于 1,执行需要排他控制的处理
} else {
// 信号量计数值为 0
}
使用示例:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [[NSMutableArray alloc] init];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int i = 0; i < 10000; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[array addObject:[NSNumber numberWithInt:i]];
dispatch_semaphore_signal(semaphore);
NSLog(@"%@",array.lastObject);
});
}
在没有 Serial Dispatch Queue 和 dispatch_barrier_async 函数那么大粒度且一部分处理需要进行排他控制的情况下,便可使用 Dispatch Semaphore。
该函数可以按指定的次数将指定的 Block 追加到指定的 Dispatch Queue 中,并等待全部处理执行结束。
dispatch_apply
可以实现类似 Dispatch Group 的效果,但和 dispatch_sync
函数一样,会堵塞当前线程直到执行结束。所以假如把 Block 派给了当前队列,就将导致死锁。若想在后台执行任务,则应使用 Dispatch Group。
推荐在 dispatch_async 函数中异步执行 dispatch_apply 函数。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu", index);
});
});
Dispatch Source
是BSD
系内核惯有功能kqueue
的包装。kqueue
是在XUN内核中发生各种事件时,在应用程序编程方执行处理的技术。
其 CPU 负荷非常小,尽量不占用资源。kqueue
可以说是应用程序处理XUN内核中发生的各种事件的方法中最优秀的一种。
当事件发生时,Dispatch Source
会将 Block 添加到到指定的队列中去执行。和手工提交到队列的任务不同,Dispatch Source
为应用提供连续的事件源。除非显式地取消,否则 Dispatch Source
会一直保留与队列的关联。
只要相应的事件发生,就会提交关联的任务到队列中执行。
为了防止事件积压到队列,Dispatch Source
实现了事件合并机制。
如果新事件在上一个事件处理器出列并执行之前到达,Dispatch Source
会将新旧事件的数据合并。
根据事件类型的不同,合并操作可能会替换旧事件,或者更新旧事件的信息。
当配置一个Dispatch Source
时,需要指定监听的事件、处理事件的队列、以及处理事件的 Block。
宏定义 | 内容 |
---|---|
DISPATCH_SOURCE_TYPE_DATA_ADD | 变量增加 |
DISPATCH_SOURCE_TYPE_DATA_OR | 变量OR |
DISPATCH_SOURCE_TYPE_MACH_SEND | MACH端口发送 |
DISPATCH_SOURCE_TYPE_MACH_RECV | MACH端口接收 |
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE | 内存报警 |
DISPATCH_SOURCE_TYPE_PROC | 进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号 |
DISPATCH_SOURCE_TYPE_READ | IO读操作,如对文件的操作、socket操作的读响应 |
DISPATCH_SOURCE_TYPE_SIGNAL | 接收到UNIX信号时响应 |
DISPATCH_SOURCE_TYPE_TIMER | 定时器 |
DISPATCH_SOURCE_TYPE_VNODE | 文件状态监听,文件被删除、移动、重命名 |
DISPATCH_SOURCE_TYPE_WRITE | IO写操作,如对文件的操作、socket操作的写响应 |
dispatch source
必须进行额外的配置才能被使用,dispatch_source_create
函数返回的 dispatch source
将处于挂起状态。此时 dispatch source
会接收事件,但是不会进行处理。
dispatch_source_set_event_handler
设置事件处理器dispatch_source_set_cancel_handler
取消处理器dispatch_suspend
/ dispatch_resume
临时地挂起和继续 dispatch source
的事件递送。
dispatch source
挂起期间,发生的事件会被累积,直到dispatch source
继续。但是不会递送所有事件,而是先合并到单一事件,然后再一次递送。比如监控一个文件的文件名变化,就只会递送最后一次的变化事件。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 定时器设定为 5s 后开始触发,2s 触发一次,允许延迟 1s。
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC);
uint64_t interval = 2ull * NSEC_PER_SEC;
uint64_t leeway = 1ull * NSEC_PER_SEC;
dispatch_source_set_timer(timer, start, interval, leeway);
// 触发时执行的 Block
dispatch_source_set_event_handler(timer, ^{
NSLog(@"wakeup");
});
// 取消时执行的 Block
dispatch_source_set_cancel_handler(timer, ^{
NSLog(@"canceled");
});
dispatch_resume(timer);
Dispatch Queue
中没有取消这一概念,一旦将处理追加到 Dispatch Queue 后就没有办法可以将该处理去除,也没有办法可以在执行中取消该处理。(可以使用 NSOperationQueue 等的其他方法)
而 Dispatch Source
是可以取消的。而且取消时需要执行的处理可指定为回调用的 Block。
使用 Dispatch Source
实现 XNU 内核中发生的事件处理要比直接使用 kqueue 实现简单。
可以挂起 / 恢复指定的 Dispatch Queue。这些函数对已经执行的处理没有影响。
dispatch_suspend
会使队列中尚未执行的任务停止执行。dispatch_resume
则使得这些任务继续执行。用于变更使用dispatch_queue_create
生成的派发队列的执行优先级。
dispatch_queue_create
函数生成的派发队列使用 default
优先级的线程。
使用这个函数还可以构建 派发队列 的执行阶层。在多个 Serial Dispatch Queue
中用 dispatch_set_target_queue
指定目标为某一个 Serial Dispatch Queue
,可以防止处理并行执行。
在指定时间后执行处理可用 dispatch_after 来实现。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"wait at least 3 seconds.")
});
dispatch_after 函数并不是在指定时间后执行处理,而只是在指定时间后追加处理到指定队列。以上代码与 3 秒后用 dispatch_async
函数添加 Block 到队列 效果相同。
dispatch_time
得到相对时间;dispatch_walltime
由 struct timespec
得到绝对时间。该函数可以保证程序中只执行一次指定任务,可以在多线程下保证安全。
实现单例模式:
+ (id) sharedInstance {
static XXObject *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
dispatch_once_t 是 int 类型的指针,对于只需执行一次的 Block,每次调用函数传入的标记必须完全相同,因此通常将标记变量声明在 static 或 global 作用域里。
使用 DIspatch I/O 和 Dispatch Data 将大文件分块进行并行读取处理。
如果想提高文件读取速度,可以尝试 Dispatch I/O。