GCD(Grand Central Dispatch)是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现。开发者只需要定义想执行的任务并追加到适当地Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,这样就比以前的线程更有效率。
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
/**
* 长时间处理
* 例如:AR用图像识别、数据库访问
*/
/**
* 长时间处理结束,主线程使用该处理结果
*/
dispatch_async(dispatch_get_main_queue(), ^{
/**
* 只在主线程可以执行的处理
* 例如用户界面刷新
*/
});
});
在导入GCD之前,Cocoa框架提供了NSObject类的performSelectorInBackground:withObject
实例方法和performSelectorOnMainThread
实例方法等简单的多线程编程技术。
线程
线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
“一个CPU执行的CPU命令列为一条无分叉路径”即为“线程”。
现在一个物理的CPU芯片实际上有64个(64核)CPU,尽管如此,“一个CPU执行的CPU命令列为一条无分叉路径”仍然不变。
OS X和iOS的核心XNU内核再爱发生操作系统事件时(如每隔一定时间,唤起系统调用等情况)会切换执行路径。执行中路径的状态,例如CPU的寄存器等信息保存到各自路径专用的内存块中,从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令列。这称为“上下文切换”。
由于使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上去好像1个CPU核能够并列地执行多个线程一样。而且在具有多个CPU核的情况下,就不是“看上去像”了,而是真的提供了多个CPU核并行执行多个线程的技术。
使用多线程容易引发的常见问题
- 数据竞争(多个线程更新相同的资源会导致数据不一致)
- 死锁(停止等待事件的线程会导致多个线程相互持续等待)
- 内存占用(使用太多线程会消耗大量内存)
尽管容易发生问题,但是为了保证应用程序的响应性能,也应当使用多线程编程。
GCD的API
Dispatch Queue
Dispatch Queue是执行处理的等待队列,按照FIFO(先进先出)的追加顺序执行处理。开发者要做的只是定义想执行的任务并追加到适当地Dispatch Queue中。
Dispatch Queue分两种:1.等待现在执行中处理结束的Serial Dispatch Queue;2.不等带现在执行中处理结束的Concurrent Dispatch Queue。
dispatch_queue_create
生成Dispatch Queue的方法。
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.MyQueue", DISPATCH_QUEUE_CONCURRENT);
/* dispatch_release(queue); */
如果你部署的最低目标低于 iOS 6.0 or Mac OS X 10.8,你应该自己管理GCD对象,使用(dispatch_retain,dispatch_release),ARC并不会去管理它们。
如果你部署的最低目标是 iOS 6.0 or Mac OS X 10.8 或者更高的版本,
ARC已经能够管理GCD对象了,这时候,GCD对象就如同普通的OC对象一样,不应该使用dispatch_retain 或者 dispatch_release
为了避免多个线程更新相同资源导致数据竞争,推荐使用Serial Dispatch Queue。
当想并发执行不发生数据竞争等问题的处理时,使用Concurrent Dispatch Queue。
Main Dispatch Queue / Global Dispatch Queue
系统提供的Dispatch Queue。
Main Dispatch Queue是在主线程中执行的Dispatch Queue。
因为主线程只有1个,所以它是Serial Dispatch Queue。
追加到Main Dispatch Queue的处理在主线程的Runloop中执行。
Global Dispatch Queue是所有应用程序都能够使用的Concurrent Dispatch Queue。没有必要通过dispatch_queue_create函数逐个生成Concurrent Dispatch Queue。只要获取Global Dispatch Queue使用即可。
表 1-1 Dispatch Queue种类
名称 | Dispatch Queue的种类 | 说明 |
---|---|---|
Main Dispatch Queue | Serial Dispatch Queue | 主线程执行 |
Global Dispatch Queue(High Priority) | Concurrent Dispatch Queue | 执行优先级:高(最高优先级) |
Global Dispatch Queue(Default Priority) | Concurrent Dispatch Queue | 执行优先级:默认 |
Global Dispatch Queue(Low Priority) | Concurrent Dispatch Queue | 执行优先级:低 |
Global Dispatch Queue(Background Priority) | Concurrent Dispatch Queue | 执行优先级:后台 |
/* 获取方法:*/
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue(); dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_set_target_queue
dispatch_queue_create函数生成的Dispatch Queue不管是Serial Dispatch Queue还是Concurrent Dispatch Queue,都使用与默认优先级Global Dispatch Queue相同执行优先级的线程。而变更生成的Dispatch Queue的执行优先级要使用dispatch_set_target_queue函数。
/* code */
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.mySerialDispatchQueue", NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);
/* end */
指定要变更执行优先级的Dispatch Queue为dispatch_set_target_queue函数的第一个参数,制定与要使用的执行优先级相同优先级的Global Dispatch Queue为第二个参数(目标优先级)。
dispatch_after
当我们想要在指定时间后执行某个处理时(切确来说,是在指定时间追加处理到Dispatch Queue),使用dispatch_after函数。
/* 在3秒后用dispatch_asyn函数追加Block到Main Dispatch Queue
* ull 是C语言的数值字面量,是显示表明类型时使用的字符串(表示“unsigned long long”)
*/
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"waited at least three seconds.");
});
第一个参数是指定时间用的dispatch_time_t类型的值。可以使用dispatch_time函数或者dispatch_walltime函数获得。
dispatch_time函数能够获取从第一个参数dispatch_time_t类型值中指定的时间开始,到第二个参数指定的毫微秒单位时间后的时间。(相对时间)
dispatch_walltime函数通常用于计算绝对时间,比如:2011年11月11日11分11秒这一绝对时间,这可以当做粗略的闹钟功能使用。dispatch_walltime函数由POSIX中使用的struct timespec类型的时间得到dispatch_time_t类型的值。
Dispatch Group
在追加到Dispatch Queue中的多个处理全部结束后想执行结束处理,这种情况会经常出现。只使用一个Serial Dispatch Queue时,只要将想执行的处理全部追加到该Serial Dispatch Queue中并在最后追加结束处理,即可实线。但是在使用Concurrent Dispatch Queue时或同时使用多个Dispatch Queue时,源代码就会变得颇为复杂。这是就用到Dispatch Group。
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(@"blk1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk2");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk3");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"done");
});
这里除了使用dispatch_group_notify,还可以使用dispatch_group_wait函数。
例如:
long result = dispatch_group_wait(group,DISPATCH_TIME_FOREVER);
永远等待下去,直到全部处理完成,所以result恒为0
例如:
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);
if (result == 0) {
// 属于Dispatch Group的全部处理结束
} else {
// 属于Dispatch Group的某个处理还在执行中
}
如果dispatch_group_wait函数的返回值不为0,就意味着虽然经过了指定的时间,
但属于Dispatch Group的某一个处理还在执行中。
如果返回值为0,那么全部处理执行结束。指定DISPATCH_TIME_NOW,则不用任何等待即可判定属于Dispatch Group的处理
是否执行结束。
long result = dispatch_group_wait(group,DISPATCH_TIME_NOW);
在主线程的Runloop的每次循环中,可检查执行是否结束,从而不耗费多余的等待时间,虽然这样可以,但一般在这种情况下,还是推荐用dispatch_group_notify函数追加技术处理到Main Dispatch Queue中,这样可以简化源代码。
dispatch_barrier_async
在访问数据库或文件时,使用Serial Dispatch Queue可避免数据竞争的问题。
写入处理确实不可与其他的处理以及包含读取处理的其他处理并行执行。但是如果读取处理只是与读取处理并行执行,那么多个并行执行就不会发生问题。也就是说,为了高效率地进行访问,读取处理追加到Concurrent Dispatch Queue中,写入处理在任何一个读取处理没有执行的状态下,追加到Serial Dispatch Queue中即可(在写入处理结束之前,读取处理不可执行)。
这时,用到dispatch_barrier_async函数。dispatch_barrier_async函数会等待追加到Concurrent Dispatch Queue上的并行执行的处理全部结束之后,再将指定的处理追加到该Concurrent Dispatch Queue中。然后在由dispatch_barrier_async函数追加的处理执行完毕后,Concurrent Dispatch Queue才恢复为一般的动作,追加到该Concurrent Dispatch Queue的处理又开始并行执行。
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
/* 写入处理 */
dispatch_async(queue, blk_for_writing);
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);
dispatch_sync(同步)
dispatch_apply(同步操作)
dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API。该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu",index);
});
dispatch_suspend / dispatch_resume
当追加大量处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已经追加的处理。这时,需要挂起Dispatch Queue,当可以执行时再恢复。
dispatch_suspend函数挂起指定的Dispatch Queue。
dispatch_suspend(queue);
dispatch_resume函数恢复指定的Dispatch Queue。
dispatch_resume(queue);
这两个函数对已经执行的处理没有影响。挂起后,追加到Dispatch Queue中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。
Dispatch Semaphore
如前所述,当并行执行的处理更新数据时,会产生数据不一致的情况,有时应用程序还会异常结束。虽然使用Serial Dispatch Queue和dispatch_barriel_asyn函数可以避免这类问题,但有必要进行更细粒度的排他控制。
Dispatch Semaphore是持有计数的信号,该计数是多线程编程中的计数类型信号。计数为0时等待,计数为1或者大于1时,减去1而不等待。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/**
* 生成Dispatch Semaphore
*
* Dispatch Semaphore的计数初始值设定为“1”。
*
* 保证可访问NSMutableArray类对象的线程同时只能有一个
*
*/
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [NSMutableArray array];
for (NSInteger i = 0; i < 100000; i++) {
dispatch_async(queue, ^{
/**
* 等待Dispatch Semaphore
* 一直等待,直到Dispatch Semaphore的计数值大于或者等于 1
*/
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/**
* 由于Dispatch Semaphore的计数值大于等于1
* 所以将Dispatch Semaphore的计数值减去1
* dispatch_semaphore_wait函数执行返回
*
* 执行到此时,计数值恒为“0”
*
* 此时可访问NSMutableArray类对象的线程只有1个
* 可以安全地进行更新
*/
[array addObject:[NSNumber numberWithInt:1]];
/**
* 排他控制结束
* 使用dispatch_semaphore_signal函数将计数值加 1
*
* 如果有通过dispatch_semaphore_wait函数等待计数值增加的线程
* 由最先等待的线程执行。
*/
dispatch_semaphore_signal(semaphore);
});
}
dispatch_once
dispatch_once函数是保证在应用程序执行中只执行一次指定处理的API。在生成单例对象时使用。在多线程下执行,也可保证百分之百安全。
static dispatch_once_t pred;
dispatch_once(&pred, ^{
/**
* 初始化
*/
});
Dispatch I/O
在读取较大文件时,如果将文件分成合适的大小使用Global Dispatch Queue并列读取的话,会比一般的读取速度快不少。
苹果中使用Dispatch I/O 和 Dispatch Data的例子
pipe_q = dispatch_queue_create("PipeQ", NULL);
pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, pipe_q, ^(int err){
close(fd);
});
*out_fd = fdpair[1];
dispatch_io_set_low_water(pipe_channel, SIZE_MAX);
dispatch_io_read(pipe_channel, 0, SIZE_MAX, pipe_q, ^(bool done, dispatch_data_t pipedata, int err){
if (err == 0)
{
size_t len = dispatch_data_get_size(pipedata);
if (len > 0)
{
const char *bytes = NULL;
char *encoded;
dispatch_data_t md = dispatch_data_create_map(pipedata, (const void **)&bytes, &len);
encoded = asl_core_encode_buffer(bytes, len);
asl_set((aslmsg)merged_msg, ASL_KEY_AUX_DATA, encoded);
free(encoded);
_asl_send_message(NULL, merged_msg, -1, NULL);
asl_msg_release(merged_msg);
dispatch_release(md);
}
}
if (done)
{
dispatch_semaphore_signal(sem);
dispatch_release(pipe_channel);
dispatch_release(pipe_q);
}
});
以上摘自Apple System Log API用的源代码(Libc-763.11 gen/asl.c)。dispatch_io_create函数创建了一个dispatch I/O。并指定发生错误的时候被执行的block,以及执行该block的队列。dispatch_io_set_low_water函数设定一次读取的大小(分割大小),dispatch_io_read函数在全局队列上开启读取操作。每当一块数据被读取后,数据作为参数会被传递给dispatch_io_read函数指定的读取结束时回调的Block。回调的用的Block分析传递过来的Dispatch Data并进行结合处理
网友评论