iOS多线程编程

此文章转载至: http://www.hrchen.com/2013/07/multi-threading-programming-of-ios-part-3/html

感谢博主node

 

2011年WWDC时推出的神器GCD。GCD: Grand Central Dispatch,是一组用于实现并发编程的C接口。GCD是基于Objective-C的Block特性开发的,基本业务逻辑和NSOperation很像,都是将工做添加到一个队列,由系统来负责线程的生成和调度。因为是直接使用Block,所以比NSOperation子类使用起来更方便,大大下降了多线程开发的门槛。另外,GCD是开源的喔:libdispatchios

基本用法

首先示例:web

1
2
3
4
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  [self doTask];  NSLog(@"Fisinished"); });

GCD的调用接口很是简单,就是将Job提交至Queue中,主要的提交Job接口为:macos

  • dispatch_sync(queue, block)同步提交job dispatch_async (queue, block) 异步提交job dispatch_after(time, queue, block) 同步延迟提交job 其中第一个参数类型是dispatch_queue_t,就是一个表示队列的数据结构typedef struct dispatch_queue_s *dispatch_queue_t;;block就是表示任务的Blocktypedef void (^dispatch_block_t)( void);

dispatch_async函数是异步非阻塞的,调用后会马上返回,工做由系统在线程池中分配线程去执行工做。 dispatch_sync和dispatch_after是阻塞式的,会一直等到添加的工做完成后才会返回。编程

除了添加Block到Dispatch Queue,还有添加函数到Dispatch Queue的接口,例如dispatch_async对应的有dispatch_async_f:数组

1
2
3
dispatch_async_f(dispatch_queue_t queue,  void *context,  dispatch_function_t work);

其中第三个参数就是个函数指针,即typedef void (*dispatch_function_t)(void *);;第二个参数是传给这个函数的参数。网络

Dispatch Queue

要添加工做到队列Dispatch Queue中,这个队列能够是串行或者并行的,并行队列会尽量的并发执行其中的工做任务,而串行队列每次只能运行一个工做任务。数据结构

目前GCD中有三种类型的Dispatch Queue:多线程

  • Main Queue:关联到主线程的队列,可使用函数dispatch_get_main_queue()得到,加到这个队列中的工做都会分发到主线程运行。主线程只有一个,所以很明显这个是串行队列,每次运行一个工做。
  • Global Queue:全局队列是并发队列,又根据优先级细分为高优先级、默认优先级和低优先级三种。经过dispatch_get_global_queue加上优先级参数得到这个全局队列,例如dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
  • 自定义Queue:本身建立一个队列,经过函数dispatch_queue_create建立,例如dispatch_queue_create("com.kiloapp.test", NULL)。第一个参数是队列的名字,Apple建议使用反DNS型的名字命名,防止重名;第二个参数是建立的queue的类型,iOS 4.3之前只支持串行,即DISPATCH_QUEUE_SERIAL(就是NULL),iOS4.3之后也开始支持并行队列,即参数DISPATCH_QUEUE_CONCURRENT。

因为有这些种不一样类型的队列,一种常见的使用模式是:

1
2
3
4
5
6
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  [self doHardWorkInBackground];  dispatch_async(dispatch_get_main_queue(), ^{  [self updateUI];  }); });

将一些耗时的工做添加到全局队列,让系统分配线程去作,工做完成后再次调用GCD的主线程队列去完成UI相关的工做,这样作就不会由于大量的非UI相关工做加剧主线程负担,从而加快UI事件响应。

其余几个可能用到的接口有:

dispatch_get_current_queue()获取当前队列,通常在提交的Block中使用。在提交的Block以外调用时,若是在主线程中就返回主线程Queue;若是是在其余子线程,返回的是默认的并发队列。

dispatch_queue_get_label(queue)获取队列的名字,若是你本身建立的队列没有设置名字,那就是返回NULL。

dispatch_set_target_queue(object, queue)设置给定对象的目标队列。这是一个很是强大的接口,目标队列负责处理这个GCD Object(参见下面的小节“管理GCD对象”),注意这个Object还能够是另外一个队列。例如我建立了了数个私有并发队列,而将它们的目标队列设置为一个串行的队列,那么我添加到这些并发队列的任务最终仍是会被串行执行。

dispatch_main()会阻塞主线程等待主队列Main Queue中的Block执行结束。

Dispatch Group

GCD确实很是简单好用,不过有些场景下仍是有点问题,例如:

1
2
3
4
5
for(id obj in array) {  [self doWorkOnItem:obj]; } [self doWorkOnArray:array];

前半部分能够用GCD获得处理性能的提高:

1
2
3
4
5
6
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); for(id obj in array)  dispatch_async(queue, ^{  [self doWorkOnItem:obj];  }); [self doWorkOnArray:array];

问题是[self doWorkOnArray:array];原先是在所有数组各个成员的工做完成后才会执行的,如今因为dispatch_async是异步的,[self doWorkOnArray:array];颇有可能在各个成员的工做完成前就开始运行,这明显不符合原先的语义。若是将dispatch_async改为dispatch_sync能够解决问题,可是和原来的方法同样没有并行处理数组,使用GCD也就没有意义了。

针对这种状况,GCD提供了Dispatch Group能够将一组工做集合在一块儿,等待这组工做完成后再继续运行。dispatch_group_create函数能够用来建立这个Group:

1
2
3
4
5
6
7
8
9
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); for(id obj in array)  dispatch_group_async(group, queue, ^{  [self doWorkOnItem:obj];  }); dispatch_group_wait(group, DISPATCH_TIME_FOREVER); dispatch_release(group); [self doWorkOnArray:array];

方法是否是很简单,将并发的工做用dispatch_group_async异步添加到一个Group和全局队列中,dispatch_group_wait会等待这些工做完成后再返回,这样你就能够再运行[self doWorkOnArray:array];

不过有点很差的是dispatch_group_wait会阻塞当前线程,若是当前是主线程岂不是很差,有更绝的dispatch_group_notify接口:

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); for(id obj in array)  dispatch_group_async(group, queue, ^{  [self doWorkOnItem:obj];  }); dispatch_group_notify(group, queue, ^{  [self doWorkOnArray:array]; }); dispatch_release(group);

dispatch_group_notify函数能够将这个Group完成后的工做也一样添加到队列中(若是是须要更新UI,这个队列也能够是主队列),总之这样作就彻底不会阻塞当前线程了。

Dispatch Group还有两个接口能够显式的告知group要添加block操做: dispatch_group_enter(group)和dispatch_group_leave(group),这两个接口的调用数必须平衡,不然group就没法知道是否是处理完全部的Block了。

Dispatch Apply

若是就是要同步的执行对数组元素的逐个操做,GCD也提供了一个简便的dispatch_apply函数:

1
2
3
4
5
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_apply([array count], queue, ^(size_t index){  [self doWorkOnItem:obj:[array objectAtIndex:index]]; }); [self doWorkOnArray:array];

Dispatch Barrier

在使用dispatch_async异步提交时,是没法保证这些工做的执行顺序的,若是须要某些工做在某个工做完成后再执行,那么可使用Dispatch Barrier接口来实现,barrier也有同步提交dispatch_barrier_async(queue, block)和异步提交dispatch_barrier_sync(queue, block)两种方式。例如:

1
2
3
4
5
dispatch_async(queue, block1); dispatch_async(queue, block2); dispatch_barrier_async(queue, block3); dispatch_async(queue, block4); dispatch_async(queue, block5);

dispatch_barrier_async是异步的,调用后马上返回,即便block3到了队列首部,也不会马上执行,而是等到block1和block2的并行执行完成后才会执行block3,完成后再会并行运行block4和block5。注意这里的queue应该是一个并行队列,并且必须是dispatch_queue_create(label, attr)建立的自定义并行队列,不然dispatch_barrier_async操做就失去了意义。

Dispatch Source

Run Loop有Input Source,GCD也一样支持一系列事件监听和处理,GCD有一组Dispatch Source接口能够监听底层系统对象(例如文件描述符、网络描述符、Mach Port、Unix信号、VFS文件系统的vnode等)的事件,能够设置这些事件的处理函数,若是事件发生时,Dispatch Source就能够将事件的处理方法提交到队列中执行。

dispatch_source_t是Dispatch Source的数据结构,使用dispatch_source_create(type, handle, mask, queue)来建立,第一个参数是source的类型:

1
2
3
4
5
6
7
8
9
10
#define DISPATCH_SOURCE_TYPE_DATA_ADD #define DISPATCH_SOURCE_TYPE_DATA_OR #define DISPATCH_SOURCE_TYPE_MACH_RECV #define DISPATCH_SOURCE_TYPE_MACH_SEND #define DISPATCH_SOURCE_TYPE_PROC #define DISPATCH_SOURCE_TYPE_READ #define DISPATCH_SOURCE_TYPE_SIGNAL #define DISPATCH_SOURCE_TYPE_TIMER #define DISPATCH_SOURCE_TYPE_VNODE #define DISPATCH_SOURCE_TYPE_WRITE

第二个参数handle和第三个参数mask与source的类型相关,有不一样的含义,第四个参数是source绑定的queue,因为篇幅问题这些含义请参考《Grand Central Dispatch (GCD) Reference》。

dispatch_source_set_event_handler(source, handler)接口能够添加source的处理方法handler,这里的handler是一个block。若是是dispatch_source_set_event_handler_f(source, handler),这里的handler就是function。

dispatch_source_cancel(source)接口能够异步取消一个source,取消后上面设置dispatch_source_set_event_handler的evnet handler就不会再执行。取消一个source时,若是以前使用dispatch_source_set_cancel_handler(source, handler)设置了一个取消时的处理block,那么这个block就会在取消source的时候提交至source关联的queue中去执行,能够用来清理资源。

dispatch_source_get_data(source)接口用于返回source须要处理的数据,根据当初建立source类型不一样有不一样的含义,并且这个接口必须在event handler中调用,不然返回结果可能未定义。

dispatch_source_get_handle(source)和dispatch_source_get_mask(source)接口分布用于获取当初建立source时的两个参数handle和mask。

dispatch_source_merge_data(source, value)接口用于将一个value值合并到souce中,这个source的类型必须是DISPATCH_SOURCE_TYPE_DATA_ADD或者DISPATCH_SOURCE_TYPE_DATA_OR。

下面举个source的例子,使用dispatch_source_get_data和dispatch_source_merge_data,假如咱们在处理上面那个数组时要在UI中显示一个进度条:

1
2
3
4
5
6
7
8
9
10
11
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue()); dispatch_source_set_event_handler(source, ^{  [progressIndicator incrementBy:dispatch_source_get_data(source)]; }); dispatch_resume(source); dispatch_apply([array count], globalQueue, ^(size_t index) {  [self doWorkOnItem:obj:[array objectAtIndex:index]];  dispatch_source_merge_data(source, 1); });

注意dispatch source建立后是处于suspend状态的,必须使用dispatch_resume来恢复,dispatch_apply中每处理一个数组元素会调用dispatch_source_merge_data加1,那么这个source的事件handler就能够经过dispatch_source_get_data拿到source的数据。

Dispatch Once

dispatch_once的意思是在App整个生命周期内运行而且只容许一次,相似于pthread库中的pthread_once)。因为dispatch_once的调试很是困难,因此最好仍是少用,单例应该是少数值得用的地方了。

传统咱们实现单例是这样:

1
2
3
4
5
6
7
8
9
10
+ (id)sharedManager {  static Manager *theManager = nil;  @synchronized([Manager class])  {  if(!theManager)  theManager = [[Manager alloc] init];  }  return theManager; } 

这个的成本仍是有点高,每次访问都会有同步锁,使用dispatch_once能够保证只运行一次初始化:

1
2
3
4
5
6
7
8
9
+ (id)sharedWhatever {  static dispatch_once_t pred;  static Manager *theManager = nil;  dispatch_once(&pred, ^{  theManager = [[Manager alloc] init];  });  return theManager; }

须要注意dispatch_once_t最好使用全局变量或者是static的,不然可能致使没法肯定的行为。

Dispatch Semaphore

和其余多线程技术同样,GCD也支持信号量,dispatch_semaphore_create(value)用于建立一个信号量类型dispatch_semaphore_t,参数是long类型,表示信号量的初始值;dispatch_semaphore_signal(semaphore)用于通知信号量(增长一个信号量);dispatch_semaphore_wait(semaphore, timeout)用于等待信号量(减小一个信号量),第二个参数是超时时间,若是返回值小于0,会按照前后顺序等待其余信号量的通知。

管理GCD对象

全部GCD的对象一样是有引用计数的,若是引用计数为0就被释放,若是你再也不须要所建立的GCD对象,就可使用dispatch_release(object)将对象的引用计数减一;一样可使用dispatch_retain(object)将对象的引用计数加一。注意因为全局和主线程队列对象都不须要去dispatch_release和dispatch_retain,即便调用了也没有做用。

dispatch_suspend(queue)能够暂停一个GCD队列的执行,固然因为是block粒度的,若是调用dispatch_suspend时正好有队列中block正在执行,那么这些运行的block结束后不会有其余的block再被执行;同理dispatch_resume(queue)能够恢复一个GCD队列的运行。注意dispatch_suspend的调用数目须要和dispatch_resume数目保持平衡,由于dispatch_suspend是计数的,两次调用dispatch_suspend会设置队列的暂停数为2,必须再调用两次dispatch_resume才能让队列从新开始执行block。

可使用dispatch_set_context(object, context)给一个GCD对象设置一个关联的数据,第二个参数任何一个内存地址;dispatch_set_context(object)就是得到这个关联数据,这样能够方便传递各种上下文数据。

本小节提到的GCD对象(Dispatch Object)不单指队列dispatch_queue_t,是指在GCD中出现的各类类型,声明类型dispatch_object_t是个union:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef union {  struct dispatch_object_s *_do;  struct dispatch_continuation_s *_dc;  struct dispatch_queue_s *_dq;  struct dispatch_queue_attr_s *_dqa;  struct dispatch_group_s *_dg;  struct dispatch_source_s *_ds;  struct dispatch_source_attr_s *_dsa;  struct dispatch_semaphore_s *_dsema;  struct dispatch_data_s *_ddata;  struct dispatch_io_s *_dchannel;  struct dispatch_operation_s *_doperation;  struct dispatch_fld_s *_dfld; } dispatch_object_t 

Dispatch Data 对象

GCD是基于C的接口,其内部处理数据是没法直接使用Objective-C的数据类型,若是要使用数据buffer时须要本身malloc一块内存空间来用,所以GCD提供了相似Objective-C中NSData的dispatch_data_t数据结构做为数据buffer。

dispatch_data_t的类型dispatch_data_s的指针,使用dispatch_data_create(buffer, size, queue, destructor)能够建立一个dispatch_data_t,第一个参数是保存数据的内存地址,第二个参数size是数据字节大小,第三个参数queue提交destructor block的队列,第四个参数destructor是用于释放data的block,默认是DISPATCH_DATA_DESTRUCTOR_DEFAULT和DISPATCH_DATA_DESTRUCTOR_FREE,后者在buffer是使用malloc生成的缓冲区时使用。示例:

1
2
void *buffer = malloc(length); dispatch_data_t data = dispatch_data_create(buffer, length, NULL, DISPATCH_DATA_DESTRUCTOR_FREE);

若是是从NSData转换为dispatch_data_t:

1
2
3
4
5
nsdata = [nsdata copy]; dispatch_queue_t queue = dispatch_get_global_queue(0, 0);  return dispatch_data_create([nsdata bytes], [nsdata length], queue, ^{  [nsdata release];  });

与直接使用己malloc分配的连续内存空间不一样,dispatch_data_t能够直接将两块数据用dispatch_data_create_concat(dataA, dataB)拼接起来,还能够用dispatch_data_create_subrange(data, offset, length)获取部分dispatch_data_t。

若是反过来要访问一个dispatch_data_t对应的内存空间,就须要使用dispatch_data_create_map(data, buffer_ptr, size_ptr)接口,示例:

1
2
3
4
5
6
7
8
9
const void *buffer; size_t length; dispatch_data_t tmpData = dispatch_data_create_map(data, &buffer, &length); //能够获得dispatch_data_t的内存空间地址和字节大小 //这里咱们能够直接使用buffer指针对应的内存 //返回的tmpData是一个新的对应data连续内存空间的dispatch_data_t dispatch_release(tmpData);

Dispatch I/O Channel

GCD提供的这组Dispatch I/O Channel接口用于异步处理基于文件和网络描述符的操做,能够用于文件和网络I/O操做。

Dispatch IO Channel对象dispatch_io_t就是对一个文件或网络描述符的封装,使用dispatch_io_t dispatch_io_create(type, fd, queue, cleanup_hander)接口生成一个dispatch_io_t对象。第一个参数type表示channel的类型,有DISPATCH_IO_STREAM和DISPATCH_IO_RANDOM两种,分布表示流读写和随机读写;第二个参数fd是要操做的文件描述符;第三个参数queue是cleanup_hander提交须要的队列;第四个参数cleanup_hander是在系统释放该文件描述符时的回调。示例:

1
2
3
4
5
dispatch_io_t fileChannel = dispatch_io_create(DISPATCH_IO_STREAM, STDIN_FILENO, dispatch_get_global_queue(0, 0), ^(int error) {  if(error)  fprintf(stderr, "error from stdin: %d (%s)\n", error, strerror(error));  }); 

dispatch_io_close(channel, flag)能够将生成的channel关闭,第二个参数是关闭的选项,若是使用DISPATCH_IO_STOP (0x01)就会马上中断当前channel的读写操做,关闭channel。若是使用的是0,那么会在正常读写结束后才会关闭channel。

During a read or write operation, the channel uses the high- and low-water mark values to determine how often to enqueue the associated handler block. It enqueues the block when the number of bytes read or written is between these two values.

在channel的读写操做中,channel会使用low_water和high_water值来决定读写了多大数据才会提交相应的数据处理block,能够dispatch_io_set_low_water(channel, low_water)和dispatch_io_set_high_water(channel, high_water)设置这两个值。

Channel的异步读写操做使用接口dispatch_io_read(channel, offset, length, queue, io_handler)和dispatch_io_write(channel, offset, data, queue, io_handler)。dispatch_io_read接口参数分布表示channel,偏移量,字节大小,提交IO处理block的队列,IO处理block;dispatch_io_write接口参数分别表示channel,偏移量,数据(dispatch_data_t),提交IO处理block的队列,IO处理block。其中io_handler的定义为^(bool done, dispatch_data_t data, int error)()

举个例子,将STDIN读到的数据写到STDERR:

1
2
3
4
5
6
dispatch_io_read(stdinChannel, 0, SIZE_MAX, dispatch_get_global_queue(0, 0), ^(bool done, dispatch_data_t data, int error) {  if(data)  {  dispatch_io_write(stderrChannel, 0, data, dispatch_get_global_queue(0, 0), ^(bool done, dispatch_data_t data, int error) {});  } }); 

看起来使用上还挺麻烦的,须要建立Channel才能进行读写,所以GCD直接提供了两个方便异步读写文件描述符的接口(参数含义和channel IO的相似):

1
2
3
4
5
6
7
8
9
10
11
12
void dispatch_read(  dispatch_fd_t fd,  size_t length,  dispatch_queue_t queue,  void (^handler)(dispatch_data_t data, int error)); void dispatch_write(  dispatch_fd_t fd,  dispatch_data_t data,  dispatch_queue_t queue,  void (^handler)(dispatch_data_t data, int error)); 

总结

GCD的API按功能分为:

  • 建立管理Queue
  • 提交Job
  • Dispatch Group
  • 管理Dispatch Object
  • 信号量Semaphore
  • 队列屏障Barrier
  • Dispatch Source
  • Queue Context数据
  • Dispatch I/O Channel
  • Dispatch Data 对象
相关文章
相关标签/搜索