并发编程之Operation Queue和GCD

来源:互联网 发布:network connect mac 编辑:程序博客网 时间:2024/05/16 15:22



转自http://blog.xcodev.com/blog/2013/10/28/operation-queue-intro/

并发编程之Operation Queue

随着移动设备的更新换代,移动设备的性能也不断提高,现在流行的CPU已经进入双核、甚至四核时代。如何充分发挥这些CPU的性能,会变得越来越重要。在iOS中如果想要充分利用多核心CPU的优势,就要采用并发编程,提高CPU的利用率。iOS中并发编程中主要有2种方式Operation Queue和GCD(Grand Central Dispatch)。下面就来先来说一下Operation Queue。

 

异步调用和并发

在深入之前,首先说说异步调用和并发。这两个概念在并发编程中很容易弄混淆。异步调用是指调用时无需等待结果返回的调用,异步调用往往会触发后台线程处理,比如NSURLConnection的异步网络回调。并发是指多个任务(线程)同时执行。在异步调用的实现中往往采用并发机制,然而并不是所有异步都是并发机制,也有可能是其他机制,比如一些依靠中断进行的操作。

 

为什么Operation Queue

Operation Queue提供一个面向对象的并发编程接口,支持并发数,线程优先级,任务优先级,任务依赖关系等多种配置,可以方便满足各种复杂的多任务处理场景。

1.面向对象接口

2.支持并发数配置

3.任务优先级调度

4.任务依赖关系

5.线程优先级配置

 

NSOperation简介

iOS并发编程中,把每个并发任务定义为一个Operation,对应的类名是NSOperation。NSOperation是一个抽象类,无法直接使用,它只定义了Operation的一些基本方法。我们需要创建一个继承于它的子类或者使用系统预定义的子类。目前系统预定义了两个子类:NSInvocationOperation和NSBlockOperation。

 

NSInvocationOperation

NSInvoationOperation是一个基于对象和selector的Operation,使用这个你只需要指定对象以及任务的selector,如果必要,你还可以设定传递的对象参数。

  1. NSInvocationOperation *invacationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doSomethingWithObj:) object:obj]; 

 

同时当这个Operation完成后,你还可以获取Operation中Invation执行后返回的结果对象。

  1. id result = [invacationOperation result]; 

 

NSBlockOperation

在一个Block中执行一个任务,这时我们就需要用到NSBlockOperation。可以通过blockOperationWithBlock:方法来方便地创建一个NSBlockOperation:

  1. NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{ 
  2.     //Do something here. 
  3. }]; 

运行一个Operation

调用Operation的start方法就可以直接运行一个Operation。

  1. [operation start]; 

 

start方法用来启动一个Operation任务。同时,Operation提供一个main方法,你的所有任务都应该在main中进行处理。默认的start方法中会先做出一些异常判断然后直接调用main方法。如果需要自定义一个NSOperation必须重载main方法来执行你所想要执行的任务。

  1. @implementation CustomOperation 
  2.  
  3. -(void)main { 
  4.    @try { 
  5.       // Do some work. 
  6.    } 
  7.    @catch(...) { 
  8.       // Exception handle. 
  9.    } 
  10. @end 

 

取消一个Operation

要取消一个Operation,要向Operation对象发送cancel消息:

  1. [operation cancel]; 

当向一个Operation对象发送cancel消息后,并不保证这个Operation对象一定能立刻取消,这取决于你的main中对cancel的处理。如果你在main方法中没有对cancel进行任何处理的话,发送cancel消息是没有任何效果的。为了让Operation响应cancel消息,那么你就要在main方法中一些适当的地方手动的判断isCancelled属性,如果返回YES的话,应释放相关资源并立刻停止继续执行。

 

创建可并发的Operation

由于默认情况下Operation的start方法中直接调用了main方法,而main方法中会有比较耗时的处理任务。如果我们在一段代码连续start了多个Operation,这些Operation都是阻塞地依次执行完,因为第二个Operation必须等到第一个Operation执行完start内的main并返回。Operation默认都是不可并发的(使用了Operation Queue情况下除外,Operation Queue会独自管理自己的线程),因为默认情况下Operation并不额外创建线程。我们可以通过Operation的isConcurrent方法来判断Operation是否是可并发的。如果要让Operation可并发,我们需要让main在独立的线程中执行,并将isConcurrent返回YES。

  1. @implementation MyOperation{ 
  2.     BOOL        executing; 
  3.     BOOL        finished; 
  4.  
  5.  
  6. - (BOOL)isConcurrent { 
  7.     return YES; 
  8.  
  9. - (void)start { 
  10.    if ([self isCancelled]) 
  11.    { 
  12.       [self willChangeValueForKey:@"isFinished"]; 
  13.       finished = YES; 
  14.       [self didChangeValueForKey:@"isFinished"]; 
  15.       return
  16.    } 
  17.  
  18.    [self willChangeValueForKey:@"isExecuting"]; 
  19.    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil]; 
  20.    executing = YES; 
  21.    [self didChangeValueForKey:@"isExecuting"]; 
  22.  
  23. - (void)main { 
  24.    @try { 
  25.         // Do some work. 
  26.  
  27.         [self willChangeValueForKey:@"isFinished"]; 
  28.         [self willChangeValueForKey:@"isExecuting"]; 
  29.         executing = NO; 
  30.         finished = YES; 
  31.         [self didChangeValueForKey:@"isExecuting"]; 
  32.         [self didChangeValueForKey:@"isFinished"]; 
  33.  
  34.    } 
  35.    @catch(...) { 
  36.       // Exception handle. 
  37.    } 
  38.  
  39. @end 

 当你自定义了start或main方法时,一定要手动的调用一些KVO通知方法,以便让对象的KVO机制可以正常运作。

 

设置Operation的completionBlock

每个Operation都可以设置一个completionBlock,在Operation执行完成时自动执行这个Block。我们可以在此进行一些完成的处理。completionBlock实现原理是对Operation的isFinnshed字段进行KVO(Key-Value Observing),当监听到isFinnished变成YES时,就执行completionBlock。

  1. operation.completionBlock = ^{ 
  2.     NSLog(@"finished"); 
  3. }; 

 

设置Operation的线程优先级

我们可以为Operation设置一个线程优先级,即threadPriority。那么执行main的时候,线程优先级就会调整到所设置的线程优先级。这个默认值是0.5,我们可以在Operation执行前修改它。

  1. operation.threadPriority = 0.1; 

注意:如果你重载的start方法,那么你需要自己来配置main执行时的线程优先级和threadPriority字段保持一致。

 

Operation状态变化

我们可以通过KVO机制来监听Operation的一下状态改变,比如一个Operation的执行状态或完成状态。这些状态的keypath包括以下几个:

  • isCancelled
  • isConcurrent
  • isExecuting
  • isFinished
  • isReady
  • dependencies
  • queuePriority
  • completionBlock

NSOperationQueue

NSOperationQueue是一个Operation执行队列,你可以将任何你想要执行的Operation添加到Operation Queue中,以在队列中执行。同时Operation和Operation Queue提供了很多可配置选项。Operation Queue的实现中,创建了一个或多个可管理的线程,为队列中的Operation提供可高度自定的执行环境。

 

Operation的依赖关系

有时候我们对任务的执行顺序有要求,一个任务必须在另一个任务执行之前完成,这就需要用到Operation的依赖(Dependency)属性。我们可以为每个Operation设定一些依赖的另外一些Operation,那么如果依赖的Operation没有全部执行完毕,这个Operation就不会被执行。

  1. [operation addDependency:anotherOperation]; 
  2. [operation removeDependency:anotherOperation]; 

如果将这些Operation和它所依赖的Operation加如队列中,那么Operation只有在它依赖的Operation都执行完毕后才可以被执行。这样我们就可以方便的控制Operation执行顺序。

 

Operation在队列中执行的优先级

Operation在队列中默认是按FIFO(First In First Out)顺序执行的。同时我们可以为单个的Operation设置一个执行的优先级,打乱这个顺序。当Queue有空闲资源执行新的Operation时,会优先执行当前队列中优先级最高的待执行Operation。

 

最大并发Operation数目

在一个Operation Queue中是可以同时执行多个Operation的,Operation Queue会动态的创建多个线程来完成相应Operation。具体的线程数是由Operation Queue来优化配置的,这一般取决与系统CPU的性能,比如CPU的核心数,和CPU的负载。但我们还是可以设置一个最大并发数的,那么Operation Queue就不会创建超过最大并发数量的线程。

  1. NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 
  2. queue.maxConcurrentOperationCount = 1

 

如果我们将maxConcurrentOperationCount设置为1,那么在队列中每次只能执行一个任务。这就是一个串行的执行队列了。

 

Simple Code

下面我写了一个简单的Simple Code来说明一下Operation和Operation Queue。

  1. NSBlockOperation *operation5s = [NSBlockOperation blockOperationWithBlock:^{ 
  2.     NSLog(@"operation5s begin"); 
  3.     sleep(5); 
  4.     NSLog(@"operation5s end"); 
  5. }]; 
  6. operation5s.queuePriority = NSOperationQueuePriorityHigh; 
  7. NSBlockOperation *operation1s = [NSBlockOperation blockOperationWithBlock:^{ 
  8.     NSLog(@"operation1s begin"); 
  9.     sleep(1); 
  10.     NSLog(@"operation1s end"); 
  11. }]; 
  12. NSBlockOperation *operation2s = [NSBlockOperation blockOperationWithBlock:^{ 
  13.     NSLog(@"operation2s begin"); 
  14.     sleep(2); 
  15.     NSLog(@"operation2s end"); 
  16. }]; 
  17.  
  18. operation1s.completionBlock = ^{ 
  19.     NSLog(@"operation1s finished in completionBlock"); 
  20. }; 
  21.  
  22. NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 
  23. queue.maxConcurrentOperationCount = 1; 
  24. [queue addOperation:operation1s]; 
  25. [queue addOperation:operation2s]; 
  26. [queue addOperation:operation5s]; 
  27. [queue waitUntilAllOperationsAreFinished]; 

运行这段代码,我得到了一下输出结果:

  1. operation1s begin 
  2. operation1s end 
  3. operation5s begin 
  4. operation1s finished in completionBlock 
  5. operation5s end 
  6. operation2s begin 
  7. operation2s end 

 

为了更好的展示队列优先级效果,我把queue的maxConcurrentOperationCount设置为1,以便任务一个一个的执行。从上面日志可以看出,第一个operation1s执行完毕后,会执行operation5s,而不是operation2s,因为operation5s的queuePriority是NSOperationQueuePriorityHigh。而第一个线程总是会第一个执行。在看看2-4行,我们可以看出operation1s的completionBlock比operation5s晚开始执行,说明它不在operation1s的线程中执行的。正如前面所说,completionBlock是通过KVO监听执行,一般会运行在监听所在线程,而不是Operation执行的线程。

 

注意事项

当一个Operation被加入Queue中后,请不要对这个Operation再进行任何修改。因为一旦加入Queue,它随时就有可能会被执行,对它的任何修改都有可能导致它的运行状态不可控制。

 

threadPriority仅仅影响了main执行时的线程优先级,其他的方法包括completionBlock都是以默认的优先级来执行的。如果自定义的话,也要注意在main执行前设置好threadPriority,执行完毕后要还原默认线程优先级。

 

经测试,Operation的threadPriority字段只有在Operation单独执行时有效,在Operation Queue中是无效的。

 

第一个加入到Operation Queue中的Operation,无论它的优先级有多么低,总是会第一个执行。

 


并发编程之GCD

在《并发编程之Operation Queue》中讲了Cocoa并发编程中的Operation Queue,了解了Operation Queue是一个面向对象的并发编程接口,它支持并发数,线程优先级,任务优先级,任务依赖关系等多种配置,可以方便满足各种复杂的多任务处理场景。本篇将接着讲另一种并发编程机制 – GCD(Grand Central Dispatch)。iOS4.0中首度引入GCD,GCD是管理任务执行的一项技术,它使得我们对多任务处理变得更加方便和有效。它支持同步或异步任务处理,串行或并行的处理队列(Dispath Queue),非系统调用的信号量机制,定时任务处理,进程、文件或网络的监听任务等。这个庞大的任务处理技术大大减少了线程的管理工作,使基于任务的开发变得更加高效。

 

Dispatch Queue

Dispatch Queue是一个任务执行队列,可以让你异步或同步地执行多个Block或函数。Dispatch Queue是FIFO的,即先入队的任务总会先执行。目前有三种类型的Dispath Queue:

1.串行队列(Serial>"com.example.MyQueue", NULL); 

dispatch_queue_create第一个参数是串行队列标识,一般用反转域名的格式表示以防冲突;第二个参数是queue的类型,设为NULL时默认是DISPATCH_QUEUE_SERIAL,将创建串行队列,在必要情况下,你可以将其设置为DISPATCH_QUEUE_CONCURRENT来创建自定义并行队列。

 

并行队列

并行队列可以同时处理多个任务,在不得以的情况下可以用dispatch_queue_create创建,但一般我们都要用系统预定义的并行队列,即全局队列(Global Concurrent Dispatch Queues)。目前系统预定义了四个不同运行优先级的全局队列,我们可以通过dispatch_get_global_queue来获取它们。

  1. dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 

 

dispatch_get_global_queue第一个参数是队列的优先级,分别对应四个全局队列:

DISPATCH_QUEUE_PRIORITY_HIGH

DISPATCH_QUEUE_PRIORITY_DEFAULT

DISPATCH_QUEUE_PRIORITY_LOW

DISPATCH_QUEUE_PRIORITY_BACKGROUND

 

dispatch_get_global_queue中第二个参数目前系统保留,请设置为0即可。

 

主队列

主队列是一个特殊的队列,它是系统预定义的运行在主线程的一个Dispatch Queue。可以通过dispatch_get_main_queue来获取唯一的主队列。主队列一般运行一些需要与主线程同步的一些短时任务。

  1. dispatch_queue_t mainQueue = dispatch_get_main_queue(); 

 

获取当前队列

你可以通过dispatch_get_current_queue获取运行时的队列:

  1. dispatch_queue_t currentQueue = dispatch_get_current_queue(); 

如果在队列执行任务中调用,返回执行此任务的队列;如果在主线程中调用,将返回主队列;如果在一般线程(非主线程线程非队列执行任务)中调用,返回DISPATCH_QUEUE_PRIORITY_DEFAULT全局队列。

 

在队列中运行任务

你可以随时向一个队列中添加一个新任务,只需要调用一下dispatch_async即可:

  1. dispatch_async(aQueue, ^{ 
  2.     //Do some work; 
  3. }); 

 

dispatch_async中的任务是异步执行的,就是说dispatch_async添加任务到执行队列后会立刻返回,而不会等待任务执行完成。然而,必要的话,你也可以调用dispatch_sync来同步的执行一个任务:

  1. dispatch_sync(aQueue, ^{ 
  2.     //Do some work; 
  3. }); 

dispatch_sync会阻塞当前线程直到提交的任务完全执行完毕。

 

Dispatch Queue的内存管理

除了系统预定义的Dispatch Queue,我们自定义的Dispatch Queue需要手动的管理它的内存。dispatch_retain和dispatch_release这两个函数可以控制Dispatch Queue的引用计数(同时可以控制后面会讲到的Dispatch Group和Dispatch Source的引用计数)。当Dispatch Queue引用计数变为0后,就会调用finalizer,finalizer是Dispatch Queue销毁前调用的函数,用来清理Dispatch Queue的相关资源。可以用dispatch_set_finalizer_f函数来设置Dispatch Queue的finalizer,这个函数同时可以设置Dispatch Group和Dispatch Source的销毁函数(后面会讲到)。

  1. void dispatch_set_finalizer_f(dispatch_object_t object, dispatch_function_t finalizer); 

 

Dispatch Queue的上下文环境数据

我们可以为每个Dispatch Queue设置一个自定义的上下文环境数据,调用dispatch_set_context来实现。同时我们也可以用dispatch_get_context获取这个上下文环境数据,这个函数同时可以设置Dispatch Group和Dispatch Source的上下文环境数据(后面会讲到)。

  1. void dispatch_set_context(dispatch_object_t object,void *context); 
  2. void * dispatch_get_context(dispatch_object_t object); 

注意Dispatch Queue并不保证这个context不会释放,不会对它进行内存管理控制。我们需要自行管理context的内存分配和释放。一般我们非配内存设置context后,可以在finalizer里释放context占有的内存。

 

并行执行循环

在编程过程中,我们经常会用到for循环,而且for循环要做很多相关的任务。比如:

  1. for (i = 0; i < count; i++) { 
  2.    //do a lot of work here. 
  3.    doSomething(i); 

 

如果for循环中处理的任务是可并发的,显然放到一个线程中处理是很慢的,GCD提供两个函数dispatch_apply和dispatch_apply_f,dispatch_apply是用于Block的,而dispatch_apply_f可以用于c函数,它们可以替代可并发的for循环,来并行的运行而提高执行效率。

  1. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
  2. dispatch_apply(count, queue, ^(size_t i) { 
  3.    //do a lot of work here. 
  4.    doSomething(i); 
  5. }); 

 

Dispatch Group

有时候我们进行下一步操作,而这个操作需要等待几个任务处理完毕后才能继续,这时我们就需要用的Dispatch Group(类似thread join)。我们可以把若干个任务放到一个Dispatch Group中:

  1. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
  2. dispatch_group_t group = dispatch_group_create(); 
  3. dispatch_group_async(group, queue, ^{ 
  4.    // Some asynchronous work 
  5. }); 

dispatch_group_async跟dispatch_async一样,会把任务放到queue中执行,不过它比dispatch_async多做了一步操作就是把这个任务和group相关联。

 

把一些任务放到Dispatch Group后,我们就可以调用dispatch_group_wait来等待这些任务完成。若任务已经全部完成或为空,则直接返回,否则等待所有任务完成后返回。注意:返回后group会清空。

  1. dispatch_group_wait(group, DISPATCH_TIME_FOREVER); 
  2. // Do some work after. 
  3. dispatch_release(group); 

 

Dispatch信号量

很多程序设计都设计到信号量,生产者-消费者模型在多线程编程中会频繁的使用。GCD提供了自己的一套信号量机制。

  1. dispatch_semaphore_t sema = dispatch_semaphore_create(RESOURCE_SIZE); 
  2. dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); 
  3. //do some work here. 
  4. dispatch_semaphore_signal(sema); 

dispatch_semaphore_wait用来获取信号量,若信号量为0,则等待直到信号量大于0。在处理任务结束后,应释放相关资源并调用dispatch_semaphore_signal使信号量增加1个。

 

Dispatch Source

Dispatch Source是GCD中监听一些系统事件的有个Dispatch对象,它包括定时器、文件监听、进程监听、Mach>

  •                                                      0, 0, queue); 
  •    if (timer) 
  •    { 
  •       dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway); 
  •       dispatch_source_set_event_handler(timer, block); 
  •       dispatch_resume(timer); 
  •    } 
  •    return timer; 
  •  
  • void MyCreateTimer() 
  •    dispatch_source_t aTimer = CreateDispatchTimer(30ull * NSEC_PER_SEC, 
  •                                1ull * NSEC_PER_SEC, 
  •                                dispatch_get_main_queue(), 
  •                                ^{ MyPeriodicTask(); }); 
  •  
  •    // Store it somewhere for later use. 
  •     if (aTimer) 
  •     { 
  •         MyStoreTimer(aTimer); 
  •     } 
  •  

    dispatch_after和dispatch_after_f

    有时候我们只想处理一次延迟任务,可以用dispatch_after和dispatch_after_f

    1. void dispatch_after( 
    2.    dispatch_time_t when, 
    3.    dispatch_queue_t queue, 
    4.    dispatch_block_t block); 

     

    监听文件事件

    监听文件事件分好几个类型,有读、写、属性的监听。

     

    读取文件

    1. dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, queue); 
    2. dispatch_source_set_event_handler(source, ^{ 
    3.    // Get some data from the source variable, which is captured 
    4.    // from the parent context. 
    5.    size_t estimated = dispatch_source_get_data(source); 
    6.    // Continue reading the descriptor... 
    7. }); 
    8. dispatch_resume(source); 

     

    写文件

    1. dispatch_source_t writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, 
    2.                         fd, 0, queue); 
    3. if (!writeSource) 
    4.     close(fd); 
    5.     return NULL; 
    6.  
    7. dispatch_source_set_event_handler(writeSource, ^{ 
    8.     size_t bufferSize = MyGetDataSize(); 
    9.     void* buffer = malloc(bufferSize); 
    10.  
    11.     size_t actual = MyGetData(buffer, bufferSize); 
    12.     write(fd, buffer, actual); 
    13.  
    14.     free(buffer); 
    15.  
    16.     // Cancel and release the dispatch source when done. 
    17.     dispatch_source_cancel(writeSource); 
    18. }); 

     

    监听文件属性

    1. dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, 
    2.             fd, DISPATCH_VNODE_RENAME, queue); 
    3. if (source) 
    4.   // Copy the filename for later use. 
    5.   int length = strlen(filename); 
    6.   char* newString = (char*)malloc(length + 1); 
    7.   newString = strcpy(newString, filename); 
    8.   dispatch_set_context(source, newString); 
    9.  
    10.   // Install the event handler to process the name change 
    11.   dispatch_source_set_event_handler(source, ^{ 
    12.         const char*  oldFilename = (char*)dispatch_get_context(source); 
    13.         MyUpdateFileName(oldFilename, fd); 
    14.   }); 
    15.  
    16.   // Install a cancellation handler to free the descriptor 
    17.   // and the stored string. 
    18.   dispatch_source_set_cancel_handler(source, ^{ 
    19.       char* fileStr = (char*)dispatch_get_context(source); 
    20.       free(fileStr); 
    21.       close(fd); 
    22.   }); 
    23.  
    24.   // Start processing events. 
    25.   dispatch_resume(source); 
    26. else 
    27.   close(fd); 

     

    监听进程事件

    1. dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, 
    2.                                                   parentPID, DISPATCH_PROC_EXIT, queue); 
    3. if (source) 
    4.    dispatch_source_set_event_handler(source, ^{ 
    5.      MySetAppExitFlag(); 
    6.      dispatch_source_cancel(source); 
    7.      dispatch_release(source); 
    8.    }); 
    9.    dispatch_resume(source); 

     

    监听中断信号

    1. dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGHUP, 0, queue); 
    2. if (source) 
    3.   dispatch_source_set_event_handler(source, ^{ 
    4.      MyProcessSIGHUP(); 
    5.   }); 
    6.  
    7.   // Start processing signals 
    8.   dispatch_resume(source); 

     

    参考文献

    Dispatch Queues:https://developer.apple.com/library/mac/documentation/general/conceptual/concurrencyprogrammingguide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW1

    Dispatch Sources:https://developer.apple.com/library/mac/documentation/general/conceptual/concurrencyprogrammingguide/GCDWorkQueues/GCDWorkQueues.html


    0 0
    原创粉丝点击