多线程编程
- 实际上CPU执行的CPU指令列为一条无分叉路径,OSX 和iOS的核心XNU内核在发生操作系统事件时会切换路径,使用多线程的程序可以再某一线程和其他线程之间反复的上下文切换,看上去像一个CPU核能并行执行多个线程。
- 在具有多CPU的情况下,是真正提供了多个CPU核并行执行多个线程的技术。
GCD的API
GCD是异步执行任务的技术之一,将应用程序中记述的线程管理用的代码在系统中实现,开发者只需要定义想要执行的任务在block中并追加到适当的DispatchQueue中,CGD就能生成必要的线程并计划执行任务。
-
Dispatch Queue 的种类
Serial Dispatch Queue Concurrent Dispatch Queue dispatch_queue_create 可以生成任意多个Dispatch Queue dispatch_queue_t queue = dispatch_queue_create("myqueue", NULL);//串行队列 dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_CONCURRENT);//并行队列 复制代码
-
MainDispatch Queue/Global Dispatch Queue Global Dispatch Queue有4个执行优先级,但是通过XNU内核用于Global Dispatch Queue的线程并不能保证实时性,执行优先级只是大致的判断。
dispatch_applyMain Dispatch Queue 是在主线程中执行的Dispacth 自然也是Serial Dispatch Queue追加到Main Dispacth Queue处理的在主线程的Runloop中执行。
dispatch_queue_create 函数生成的Dispatch Queue执行的优先级都与默认优先级的Global Dispatch queue相同。 dispatch_set_target_queue 变更生成的Dispatch Queue 执行的优先级顺序
//queue为指定要变更的queue 将queue的优先级设置成和globalQueue一样的优先级 dispatch_set_target_queue(queue, globalQueue); 复制代码
-
使用Dispatch Group
a.dispatch_queue_t
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(@"1"); }); dispatch_group_async(group, queue, ^{ NSLog(@"2"); }); dispatch_group_notify(group, queue, ^{ NSLog(@"end"); }); 复制代码
b.dispatch_group_wait表示在经过指定时间或属于指定Dispatch Group处理的全部执行结束之前,执行该函数的线程停止。
long result = dispatch_group_wait(group, DISPATCH_TIME_FOREVER); long result = dispatch_group_wait(group, DISPATCH_TIME_NOW);//不用任何等待即可判定属于Dispatch Group的处理 复制代码
是否执行结束在主线程的Runloop的每次循环中,可检查执行是否结束,从而不耗费多余的等待时间。
-
dispatch_apply
NSArray *array = @[@"11",@"22",@"33"]; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_apply([array count], queue, ^(size_t index) { NSLog(@"%zu:%@",index,[array objectAtIndex:index]); }); 复制代码
第一个参数是重复次数,第二个参数是为追加对象的Dispatch Queue,第三个参数为追加的处理。
上面可简单的在dispatch_get_global_queue中对所有元素执行Block,dispatch_apply和dispatch_sync函数相同,会等待处理执行结束,推荐在dispatch_async函数中非同步地执行dispatch_apply。
NSArray *array = @[@"11",@"22",@"33"]; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //在Global Dispatch Queue 中非同步执行 dispatch_async(queue, ^{ //等待dispatch_apply函数中全部处理执行结束 dispatch_apply([array count], queue, ^(size_t index) { //并列处理包含在NSArray对象的全部对象 NSLog(@"%zu:%@",index,[array objectAtIndex:index]); }); //dispatch_apply函数中的处理全部执行结束 dispatch_async(dispatch_get_main_queue(), ^{ //回到主线程 NSLog(@"done"); }); }); 复制代码
-
dispatch_barrier_async
函数会等待追加到并发队列上的并发执行的处理全部结束之后再讲指定的处理追加到该queue上,然后由dispatch_barrier_async函数追加的处理执行完毕后,追加到queue上的处理又开始并发执行。dispatch_barrier_async 函数的处理流程:
使用dispatch_barrier_async和Concurrent Dispatch Queue函数可实现高效率的数据访问和文件访问。
dispatch_barrier_async和dispatch_barrier_sync的区别:
a).两个函数都会等等待在它前面插入队列的任务先执行完,会等待他们自己的任务执行完之后再执行后面的任务
b).在将任务插入到queue的时候,dispatch_barrier_sync需要等待自己的任务结束之后才会继续程序,然后插入被写在它之后的任务,在执行 dispatch_barrier_async将自己的任务插入到队列之后,不会等待自己的任务结束就会继续把后面的任务插入到queue,等待自己的任务执行结束执行后面插入的任务。
-
dispatch_sync 和 dispatch_async
dispatch_async:指的是将指定的Block""非同步"地追加到指定queue中,这个函数不做任何等待。 dispatch_sync:将指定的block"同步追加到指定的queue中,在追加block结束之前,dispatch_sync会一直等待,如dispatch_group_wait函数类似。
//如下代码会导致死锁,该源码在Main Dispatch Queue 即主线程中执行指定的Block,并等待其执行结束,但其实主线程中正在执行这些源码, //所以无法执行追加到main queue的block,类似在serial Dispatch queue中也会有同样的问题存在 dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_sync(queue, ^{ NSLog(@"hello"); }); 复制代码
-
dispatch_suspend / dispatch_resume
dispatch_suspend函数挂起指定的queue dispatch_resume函数恢复指定的queue 这个两个函数对已经执行的处理没有影响,挂起后,追加到queue中但未执行的处理在此之后停止执行,而恢复则使得这些处理能够继续执行。 -
dispatch_semaphore_t
dispatch_semaphore_create 创建一个semaphor dispatch_semaphore_signal 发送一个信号 dispatch_semaphore_wait 等待信号dispatch_semaphore_t是持有计数的信号,计数为0时等待,计数大于等于1的时候不等待
下面这段代码执行后由内存错误导致应用程序异常结束的概率很高
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); NSMutableArray *arr = [NSMutableArray array]; for (int i = 0; i < 100000; i ++) { dispatch_async(queue1, ^{ [arr addObject:[NSNumber numberWithInt:i]]; }); } 复制代码
使用Dispatch Semaphore 可以解决这个问题
dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); NSMutableArray *arr = [NSMutableArray array]; for (int i = 0; i < 100000; i ++) { dispatch_async(queue1, ^{ //等待Dispatch Semaphore 直到Dispatch Semaphore的计数值大于等于1 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //Dispatch Semaphore的计数值达到大于等于1,Dispatch Semaphore减去1,dispatch_semaphore_wait函数执行返回 //即执行到此时的Dispatch Semaphore计数值恒为"0",由于可访问NSMutableArray类对象的线程只有1个,所以可以安全更新 [arr addObject:[NSNumber numberWithInt:i]]; dispatch_semaphore_signal(semaphore); }); } 复制代码
-
Dispatch I/O
在使用多个线程更快地并列读取文件,可以通过Dispatch I/O 和 Dispatch Data。 在通过Dispatch I/O 读写文件时,使用Global Dispatch Queue将1一个文件按照某一个大小read/write 下面是苹果使用Dispatch I/O 和Disaptch Data的部分例子:if (where == ASL_STORE_LOCATION_MEMORY) { /* create a pipe */ asl_aux_context_t *ctx = (asl_aux_context_t *)calloc(1, sizeof(asl_aux_context_t)); if (ctx == NULL) return -1; status = pipe(fdpair); if (status < 0) { free(ctx); return -1; } /* give read end to dispatch_io_read */ fd = fdpair[0]; sem = dispatch_semaphore_create(0); ctx->sem = sem; ctx->fd = fdpair[1]; status = _asl_aux_save_context(ctx); if (status != 0) { close(fdpair[0]); close(fdpair[1]); dispatch_release(sem); free(ctx); return -1; } //创建一个串行队列 pipe_q = dispatch_queue_create("PipeQ", NULL); //创建一个dispatch I/O 复制代码
创建一个持有文件描述符的通道,在创建之后,不准以任何方式修改这个文件描述符,两种类型不同的通道。
DISPATCH_IO_STREAM 0 流 如果你打开了一个套接字,可以创建一个流通道DISPATCH_IO_RANDOM 1 随机存取如果你打开的是硬盘上的文件,可以使用它来创建一个随机存取的通道(因为这样的文件描述符是可寻址的),如果你想创建一个文件通道,最好使用一个路径参数dispatch_io_create_with_path,并且让GCD来打开这个文件。
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); //无论数据何时读完和写完,读写操作调用一个block来结束,这些都是以非阻塞,异步I/O的形式高效实现的 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) { //定义一个字节数组bytes const char *bytes = NULL; char *encoded; //dispatch_io_read 函数指定的读取结束时回调用的block中拿到每一块读取好的数据,并进行合并处理 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); } }); return 0; } 复制代码
GCD的实现
一. Dispatch Queue
a.用于管理追加的Block的C语言层实现的FIFO队列
b.atomic函数中实现的用于排他控制的轻量级信号
c.用于管理线程的C语言层实现的一些容器
d.用于实现Dispatch Queue 而使用的软件组件
组件名称 | 提供技术 |
---|---|
libdispatch | Dispatch Queue |
Libc(pthreads) | pthread_workqueue |
XNU内核 | workqueue |
FIFO队列管理是通过dispatch_async等函数所追加的Block,Block 并不是直接加入到FIFO队列中,而是加入到dispatch_continuation_t类型的结构体中,然后在加入到FIFO队列中。
1.队列优先级
dispatch_continuation_t用于记录Block所属的Dispatch Group和一些其他信息(类似执行的上下文)。
Global Dispatch Queue 有8种优先级:
High Priority
Default Priority
Low Priority
Background Priority
High Overcommit Priority
Default Overcommit Priority
Low Overcommit Priority
Background Overcommit Priority
复制代码
优先级中附有Overcommit的Global Dispatch Queue 使用在serial Dispatch Queue 中,不管系统状态如何,都会强制生成线程的Dispatch Queue。
2.pthread_workqueue工作队列
是一个用于创建内核线程的接口,通过它创建的内核线程来执行内核其他模块排列到队列里的工作 使用 pthread_workqueue_create_np 创建pthread_workqueue
a.pthread_workqueue包含在Libc提供的pthreads API中,其使用bsdthread_register和workq_open 系统调用。
b.在初始化XNU内核的workqueue之后获取workqueue信息。
XNU内核持有4种workqueue:
WORKQUEUE_HIGH_PRIOQUEUE
WORKQUEUE_DEFALUT_PRIOQUEUE
WORKQUEUE_LOW_PRIOQUEUE
WORKQUEUE_BG_PRIOQUEUE
复制代码
二. Block的执行过程
a.libdispatch从Global Dispatch Queue 自身的FIFO队列中取出Dispatch Continuation 。
b.调用pthread_workqueue_additem_np函数,传入这些参数:dispatch queue自身、一个符合其优先级workqueue,执行dispatch continuation的回调函数。
注:回调函数是一个通过函数指针调用的函数,如果你把函数的指针作为参数传递给另一个函数,这个指针被用来调用其所指向的函数
c.pthread_workqueue_additem_np函数使用workq_kernreturn系统调用,通知workqueue应当执行的项目,XNU内核基于系统状态判断是否要生成线程,如果是Overcommit优先级的Global Dispatch Queue,workqueue会始终生成线程.
d.workqueue的线程执行pthread_workqueue 函数,该函数调用libdispatch的回调函数,在该回调函数中执行加入到的Dispatch Continuation 的Block。
e.Block执行结束后,进行通知Dispatch Group结束 ,释放Dispatch Continuation 等处理 ,执行下一个Block。
三. Dispatch Source
它是BSD系内核惯有功能kqueue的包装,kqueue是在XNU内核中发生各种事件时,在应用程序编程方执行处理的技术。
Dispatch Source可以处理的一下事件:
名称 | 内容 |
---|---|
DISPATCH_SOURCE_TYPE_DATA_ADD | 变量增加 |
DISPATCH_SOURCE_TYPE_DATA_OR | 变量OR |
DISPATCH_SOURCE_TYPE_MACT_SEND | MACH端口发送 |
DISPATCH_SOURCE_TYPE_MACT_RECV | MACH端口接收 |
DISPATCH_SOURCE_TYPE_PROC | 检测到与进程相关的事件 |
DISPATCH_SOURCE_TYPE_READ | 可读取文件映像 |
DISPATCH_SOURCE_TYPE_SIGNAL | 接受信号 |
DISPATCH_SOURCE_TYPE_TIMER | 定时器 |
DISPATCH_SOURCE_TYPE_VNONE | 文件系统有变更 |
DISPATCH_SOURCE_TYPE_WRITE | 可写入文件映像 |
事件发生时,在指定的DispatchQueue中执行事件的处理。
- 使用Dispatch Source写入文件
Dispatch Source同样使用在了Core Foundation 框架的用于异步网络的API CFSocket 中,Fpundation框架的异步网络API是通过CFSocket 实现的。- (void)writeDispatchSource { NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString *fileName = [filePath stringByAppendingString:@"/test.text"]; int sockfd = open([fileName UTF8String], O_WRONLY | O_CREAT | O_TRUNC,(S_IRUSR | S_IWUSR | S_ISGID)); if (sockfd == -1) { return; } fcntl(sockfd, F_SETFL); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, sockfd, 0, queue); dispatch_source_set_event_handler(source, ^{ size_t bufferSize = 100; void *buffer = malloc(bufferSize); static NSString *content = @"Write Data Action: "; content = [content stringByAppendingString:@"=New info="]; NSString *writeContent = [content stringByAppendingString:@"\n"]; void *string = [writeContent UTF8String]; size_t actual = strlen([writeContent UTF8String]); memcpy(buffer, string, actual); write(sockfd, buffer, actual); free(buffer); //如果没有这句话 ,只要文件可写,写操作会一直进行,直到磁盘满,本例中,只要超过buffer的容量就会崩溃 dispatch_suspend(source); }); dispatch_source_set_cancel_handler(source, ^{ close(sockfd); }); if (!source) { close(sockfd); } } 复制代码
四. DISPATCH_SOURCE_TYPE_TIMER定时器
1.下面是timer的一个使用方式:
/*
*将定时器设定为15s
*不指定为重复
*允许迟延1s
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 1 * NSEC_PER_SEC);
//指定定时器指定时间内执行的处理
dispatch_source_set_event_handler(timer, ^{
NSLog(@"wakeup");
dispatch_source_cancel(timer);
});
dispatch_source_set_cancel_handler(timer, ^{
NSLog(@"cancel");
//dispatch_release(timer);
});
//启动定时器
dispatch_resume(timer);
复制代码
注意;实际上Dispatch Queue 没有取消的概念,一旦将处理追加到Dispatch Queue 中,就没有办法将该处理去除,也没办法在执行中取消该处理。