iOS 并发编程(2)

Dispatch Queues 编程

dispatch queues是执行任务的强大工具,容许你同步或异步地执行任意代码block。原先使用单独线程执行的全部任务均可以替换为使用dispatch queues。而dispatch queues最大的优势在于使用简单,并且更加高效。 缓存

dispatch queues任务的概念就是应用须要执行的一些工做,如计算、建立或修改数据结构、处理数据等等。咱们使用函数或block对象来定义任务,并添加到dispatch queue。 安全

dispatch queue是相似于对象的结构体,管理你提交给它的任务,并且都是先进先出的数据结构。所以queue中的任务老是以添加的顺序开始执行。Grand Central Disaptch提供了几种dispatch queues,不过你也本身建立。 数据结构

类型 描述
串行 也称为private dispatch queue,每次只执行一个任务,按任务添加顺序执行。当前正在执行的任务在独立的线程中运行(不一样任务的线程可能不一样),dispatch queue管理了这些线程。一般串行queue主要用于对特定资源的同步访问。
你能够建立任意数量的串行queues,虽然每一个queue自己每次只能执行一个任务,可是各个queue之间是并发执行的。
并发 也称为global dispatch queue,能够并发执行一个或多个任务,可是任务仍然是以添加到queue的顺序启动。每一个任务运行于独立的线程中,dispatch queue管理全部线程。同时运行的任务数量随时都会变化,并且依赖于系统条件。
你不能建立并发dispatch queues。相反应用只能使用三个已经定义好的全局并发queues。
Main dispatch queue 全局可用的串行queue,在应用主线程中执行任务。这个queue与应用的 run loop 交叉执行。因为它运行在应用的主线程,main queue一般用于应用的关键同步点。
虽然你不须要建立main dispatch queue,但你必须确保应用适当地回收

应用使用dispatch queue,相比线程有不少优势,最直接的优势是简单,不用编写线程建立和管理的代码,让你集中精力编写实际工做的代码。另外系统管理线程更加高效,而且能够动态调控全部线程。 并发

dispatch queue比线程具备更强的可预测性,例如两个线程访问共享资源,你可能没法控制哪一个线程前后访问;可是把两个任务添加到串行queue,则能够确保两个任务对共享资源的访问顺序。同时基于queue的同步也比基于锁的线程同步机制更加高效。 app

应用有效地使用dispatch queue,要求尽量地设计自包含、能够异步执行的任务。 异步

dispatch queues的几个关键点: async

dispatch queues相对其它dispatch queues并发地执行任务,串行化任务只能在同一个dispatch queue中实现。 ide

系统决定了同时可以执行的任务数量,应用在100个不一样的queues中启动100个任务,并不表示100个任务所有都在并发地执行(除非系统拥有100或更多个核) 异步编程

系统在选择执行哪一个任务时,会考虑queue的优先级。

queue中的任务必须在任什么时候候都准备好运行,注意这点和Operation对象不一样。

private dispatch queue是引用计数的对象。你的代码中须要retain这些queue,另外dispatch source也可能添加到一个queue,从而增长retain的计数。所以你必须确保全部dispatch source都被取消,并且适当地调用release。

Queue相关的技术

除了dispatch queue,Grand Central Disaptch还提供几个相关的技术,使用queue来帮助你管理代码。

技术 描述
Dispatch group 用于监控一组block对象完成(你能够同步或异步地监控block)。Group提供了一个很是有用的同步机制,你的代码能够等待其它任务的完成
Dispatch semaphore 相似于传统的semaphore(信号量),可是更加高效。只有当调用线程因为信号量不可用,须要阻塞时,Dispatch semaphore才会去调用内核。若是信号量可用,就不会与内核进行交互。使用信号量能够实现对有限资源的访问控制
Dispatch source Dispatch source在特定类型的系统事件发生时,会产生通知。你可使用dispatch source来监控各类事件,如:进程通知、信号、描述符事件、等等。当事件发生时,dispatch source异步地提交你的任务到指定的dispatch queue,来进行处理。
使用Block实现任务

Block能够很是容易地定义“自包含”的工做单元,尽管看上去很是相似于函数指针,block实际上由底层数据结构来表示,由编译器负责建立和管理。编译器对你的代码(和全部相关的数据)进行打包,封装为能够存在于堆中的格式,并在你的应用各个地方传递。

Block最关键的优势可以使用own lexical scope以外的变量,在函数或方法内部定义一个block时,block能够直接读取父scope中的变量。block访问的变量所有被拷贝到block在堆中的数据结构,这样block就能在稍后自由地访问这些变量。当block被添加到dispatch queue中时,这些变量一般是只读格式的。不过同步执行的Block对象,可使用那些定义为__block的变量,对这些变量的修改会影响到调用scope。

Block的简单用法:

  
  1. int x = 123; 
  2. int y = 456; 
  3.   
  4. // Block declaration and assignment 
  5. void (^aBlock)(int) = ^(int z) { 
  6.     printf("%d %d %d\n", x, y, z); 
  7. }; 
  8.   
  9. // Execute the block 
  10. aBlock(789);   // prints: 123 456 789 

设计Block时需考虑如下关键指导方针:

对于使用dispatch queue的异步Block,能够在Block中安全地捕获和使用父函数或方法中的scalar变量。可是Block不该该去捕获大型结构体或其它基于指针的变量,这些变量由Block的调用上下文分配和删除。在你的Block被执行时,这些指针引用的内存可能已经不存在。固然,你本身显式地分配内存(或对象),而后让Block拥有这些内存的全部权,是安全可行的。

Dispatch queue对添加的Block会进行复制,在完成执行后自动释放。换句话说,你不须要在添加Block到Queue时显式地复制

尽管Queue执行小任务比原始线程更加高效,仍然存在建立Block和在Queue中执行的开销。若是Block作的事情太少,可能直接执行比dispatch到queue更加有效。使用性能工具来确认Block的工做是否太少

绝对不要针对底层线程缓存数据,而后指望在不一样Block中可以访问这些数据。若是相同queue中的任务须要共享数据,应该使用dispatch queue的context指针来存储这些数据。

若是Block建立了大量Objective-C对象,考虑建立本身的autorelease pool,来处理这些对象的内存管理。虽然dispatch queue也有本身的autorelease pool,但不保证在何时会回收这些pool。

建立和管理Dispatch Queue

得到全局并发Dispatch Queue

并发dispatch queue能够同时并行地执行多个任务,不过并发queue仍然按先进先出的顺序来启动任务,并发queue会在以前任务完成以前就出列下一个任务并启动执行。并发queue同时执行的任务数量会根据应用和系统动态变化,各类因素包括:可用核数量、其它进程正在执行的工做数量、其它串行dispatch queue中优先任务的数量等。

系统给每一个应用提供三个并发dispatch queue,全部应用全局共享,三个queue的区别是优先级。你不须要显式地建立这些queue,使用 dispatch_get_global_queue 函数来获取这三个queue:

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

除了默认优先级的并发queue,你还能够得到高和低优先级的两个,分别使用 DISPATCH_QUEUE_PRIORITY_HIGH 和 DISPATCH_QUEUE_PRIORITY_LOW 常量来调用上面函数。

虽然dispatch queue是引用计数的对象,但你不须要retain和release全局并发queue。由于这些queue对应用是全局的,retain和release调用会被忽略。

你也不须要存储这三个queue的引用,每次都直接调用 dispatch_get_global_queue 得到queue就好了。

建立串行Dispatch Queue

应用的任务须要按特定顺序执行时,就须要使用串行Dispatch Queue,串行queue每次只能执行一个任务。你可使用串行queue来替代锁,保护共享资源或可变的数据结构。和锁不同的是,串行queue确保任务按可预测的顺序执行。并且只要你异步地提交任务到串行queue,就永远不会产生死锁。

你必须显式地建立和管理全部你使用的串行queue,应用能够建立任意数量的串行queue,但不要为了同时执行更多任务而建立更多的串行queue。若是你须要并发地执行大量任务,应该把任务提交到全局并发Queue。

建立串行queue时,你须要明确本身的目的,如保护共享资源,或同步应用的某些关键行为。

dispatch_queue_create 函数建立串行queue,两个参数分别是queue名和一组queue属性。调试器和性能工具会显示queue的名字,便于你跟踪任务的执行。

 
  1. dispatch_queue_t queue; 
  2. queue = dispatch_queue_create("com.example.MyQueue", NULL); 

运行时得到公共Queue

Grand Central Disaptch提供函数,让应用访问几个公共dispatch queue:

使用 dispatch_get_current_queue 函数做为调试用途,或者测试当前queue的标识。在block对象中调用这个函数会返回block提交到的queue(这个时候queue应该正在执行中)。在block对象以外调用这个函数会返回应用的默认并发queue。

使用 dispatch_get_main_queue 函数得到应用主线程关联的串行dispatch queue。Cocoa 应用、调用了 dispatch_main 函数或配置了run loop(CFRunLoopRef 类型 或一个 NSRunLoop 对象)的应用,会自动建立这个queue。

使用 dispatch_get_global_queue 来得到共享的并发queue

Dispatch Queue的内存管理

Dispatch Queue和其它dispatch对象都是引用计数的数据类型。当你建立一个串行dispatch queue时,初始引用计数为1,你可使用 dispatch_retain 和 dispatch_release 函数来增长和减小引用计数。当引用计数到达0时,系统会异步地销毁这个queue。

对dispatch对象(如queue)retain和release是很重要的,确保它们被使用时可以保留在内存中。和内存托管的Cocoa对象同样,通用的规则是若是你使用一个传递给你代码中的queue,你应该在使用前retain,使用完以后release。

你不须要retain或release全局dispatch queue,包括全局并发 dispatch queue和main dispatch queue。

即便你实现的是自动垃圾收集的应用,也须要retain和release你的dispatch queue和其它dispatch对象。Grand Central Disaptch不支持垃圾收集模型来回收内存。

在Queue中存储自定义上下文信息

全部dispatch对象(包括dispatch queue)都容许你关联custom context data。使用 dispatch_set_context 和 dispatch_get_context 函数来设置和获取对象的上下文数据。系统不会使用你的上下文数据,因此须要你本身在适当的时候分配和销毁这些数据。

对于Queue,你可使用上下文数据来存储一个指针,指向Objective-C对象或其它数据结构,协助标识这个queue或代码的其它用途。你可使用queue的finalizer函数来销毁(或解除关联)上下文数据。

为Queue提供一个清理函数

在建立串行dispatch queue以后,能够附加一个finalizer函数,在queue被销毁以前执行自定义的清理操做。使用 dispatch_set_finalizer_f 函数为queue指定一个清理函数,当queue的引用计数到达0时,就会执行该清理函数。你可使用清理函数来解除queue关联的上下文数据,并且只有上下文指针不为NULL时才会调用这个清理函数。

下面例子演示了自定义finalizer函数的使用,你须要本身提供 myInitializeDataContextFunction 和 myCleanUpDataContextFunction 函数,用于初始化和清理上下文数据。

 
  1. void myFinalizerFunction(void *context) 
  2.     MyDataContext* theData = (MyDataContext*)context; 
  3.   
  4.     // Clean up the contents of the structure 
  5.     myCleanUpDataContextFunction(theData); 
  6.   
  7.     // Now release the structure itself. 
  8.     free(theData); 
  9.   
  10. dispatch_queue_t createMyQueue() 
  11.     MyDataContext*  data = (MyDataContext*) malloc(sizeof(MyDataContext)); 
  12.     myInitializeDataContextFunction(data); 
  13.   
  14.     // Create the queue and set the context data. 
  15.     dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL); 
  16.     if (serialQueue) 
  17.     { 
  18.         dispatch_set_context(serialQueue, data); 
  19.         dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction); 
  20.     } 
  21.   
  22.     return serialQueue; 

添加任务到Queue

要执行一个任务,你须要将它dispatch到一个适当的dispatch queue,你能够同步或异步地dispatch一个任务,也能够单个或按组来dispatch。一旦进入到queue,queue会负责尽快地执行你的任务。

添加单个任务到Queue

你能够异步或同步地添加一个任务到Queue,尽量地使用 dispatch_async 或 dispatch_async_f 函数异步地dispatch任务。由于添加任务到Queue中时,没法肯定这些代码何时可以执行。所以异步地添加block或函数,可让你当即调度这些代码的执行,而后调用线程能够继续去作其它事情。

特别是应用主线程必定要异步地dispatch任务,这样才能及时地响应用户事件。

少数时候你可能但愿同步地dispatch任务,以免竞争条件或其它同步错误。使用 dispatch_sync 和 dispatch_sync_f 函数同步地添加任务到Queue,这两个函数会阻塞,直到相应任务完成执行。

绝对不要在任务中调用 dispatch_sync 或 dispatch_sync_f 函数,并同步dispatch新任务到当前正在执行的queue。对于串行queue这一点特别重要,由于这样作确定会致使死锁;而并发queue也应该避免这样作。

 
  1. dispatch_queue_t myCustomQueue; 
  2. myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL); 
  3.   
  4. dispatch_async(myCustomQueue, ^{ 
  5.     printf("Do some work here.\n"); 
  6. }); 
  7.   
  8. printf("The first block may or may not have run.\n"); 
  9.   
  10. dispatch_sync(myCustomQueue, ^{ 
  11.     printf("Do some more work here.\n"); 
  12. }); 
  13. printf("Both blocks have completed.\n"); 

任务完成时执行Completion Block

dispatch到queue中的任务,一般与建立任务的代码独立运行。在任务完成时,应用可能但愿获得通知并使用任务完成的结果数据。在传统的异步编程模型中,你可能会使用回调机制,不过dispatch queue容许你使用Completion Block。

Completion Block是你dispatch到queue的另外一段代码,在原始任务完成时自动执行。调用代码在启动任务时经过参数提供Completion Block。任务代码只须要在完成工做时提交指定的Block或函数到指定的queue。

下面代码使用block实现了平均数,最后两个参数容许调用方指定一个queue和报告结果的block。在平均数函数完成计算后,会传递结果到指定的block,并dispatch到指定的queue。为了防止queue被过早地释放,必须首先retain这个queue,而后在dispatch这个Completion Block以后,再release这个queue。

 
  1. void average_async(int *data, size_t len, 
  2.    dispatch_queue_t queue, void (^block)(int)) 
  3.    // Retain the queue provided by the user to make 
  4.    // sure it does not disappear before the completion 
  5.    // block can be called. 
  6.    dispatch_retain(queue); 
  7.   
  8.    // Do the work on the default concurrent queue and then 
  9.    // call the user-provided block with the results. 
  10.    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 
  11.       int avg = average(data, len); 
  12.       dispatch_async(queue, ^{ block(avg);}); 
  13.   
  14.       // Release the user-provided queue when done 
  15.       dispatch_release(queue); 
  16.    }); 

并发地执行Loop Iteration

若是你使用循环执行固定次数的迭代,并发dispatch queue可能会提升性能。例以下面for循环:

 
  1. for (i = 0; i < count; i++) { 
  2.    printf("%u\n",i); 

若是每次迭代执行的任务与其它迭代独立无关,并且循环迭代执行顺序也可有可无的话,你能够调用 dispatch_apply 或 dispatch_apply_f 函数来替换循环。这两个函数为每次循环迭代将指定的block或函数提交到queue。当dispatch到并发queue时,就有可能同时执行多个循环迭代。

调用 dispatch_apply 或 dispatch_apply_f 时你能够指定串行或并发queue。并发queue容许同时执行多个循环迭代,而串行queue就没太大必要使用了。

和普通for循环同样,dispatch_apply 和 dispatch_apply_f 函数也是在全部迭代完成以后才会返回。所以在queue上下文执行的代码中再次调用这两个函数时,必须很是当心。若是你传递的参数是串行queue,并且正是执行当前代码的Queue,就会产生死锁。

另外这两个函数还会阻塞当前线程,所以在主线程中调用这两个函数一样必须当心,可能会阻止事件处理循环并没有法响应用户事件。因此若是循环代码须要必定的时间执行,你能够考虑在另外一个线程中调用这两个函数。

下面代码使用 dispatch_apply 替换了for循环,你传递的block必须包含一个参数,用来标识当前循环迭代。第一次迭代这个参数值为0,第二次时为1,最后一次值为count - 1。

 
  1. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
  2.   
  3. dispatch_apply(count, queue, ^(size_t i) { 
  4.    printf("%u\n",i); 
  5. }); 

循环迭代执行的工做量须要仔细平衡,太多的话会下降响应性;太少则会影响总体性能,由于调度的开销大于实际执行代码。

在主线程中执行任务

Grand Central Disaptch提供一个特殊dispatch queue,能够在应用的主线程中执行任务。应用主线程设置了run loop(由CFRunLoopRef 类型或 NSRunLoop 对象管理),就会自动建立这个queue,而且自动drain。非Cocoa应用若是不显式地设置run loop,就必须显式地调用dispatch_main 函数来显式地drain这个dispatch queue。不然虽然你能够添加任务到queue,但任务永远不会被执行。

调用 dispatch_get_main_queue 函数得到应用主线程的dispatch queue。添加到这个queue的任务由主线程串行化执行,所以你能够在应用的某些地方使用这个queue做为同步点。

任务中使用Objective-C对象

Grand Central Disaptch支持Cocoa内存管理机制,所以能够在提交到queue的block中自由地使用Objective-C对象。每一个dispatch queue维护本身的autorelease pool确保释放autorelease对象,可是queue不保证这些对象实际释放的时间。在自动垃圾收集的应用中,Grand Central Disaptch会在垃圾收集系统中注册本身建立的每一个线程。

若是应用消耗大量内存,而且建立大量autorelease对象,你须要建立本身的autorelease pool,用来及时地释放再也不使用的对象。

挂起和继续queue

咱们能够暂停一个queue以阻止它执行block对象,使用 dispatch_suspend 函数挂起一个dispatch queue;使用 dispatch_resume 函数继续dispatch queue。调用 dispatch_suspend 会增长queue的引用计数,调用 dispatch_resume 则减小queue的引用计数。当引用计数大于0时,queue就保持挂起状态。所以你必须对应地调用suspend和resume函数。

挂起和继续是异步的,并且只在执行block之间生效。挂起一个queue不会致使正在执行的block中止。

使用Dispatch Semaphore控制有限资源的使用

若是提交到dispatch queue中的任务须要访问某些有限资源,可使用dispatch semaphore来控制同时访问这个资源的任务数量。dispatch semaphore和普通的信号量相似,惟一的区别是当资源可用时,须要更少的时间来得到dispatch semaphore。

使用dispatch semaphore的过程以下:

使用 dispatch_semaphore_create 函数建立semaphore,指定正数值表示资源的可用数量。

在每一个任务中,调用 dispatch_semaphore_wait 来等待Semaphore

当上面调用返回时,得到资源并开始工做

使用完资源后,调用 dispatch_semaphore_signal 函数释放和signal这个semaphore

 
  1. // Create the semaphore, specifying the initial pool size 
  2. dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2); 
  3.   
  4. // Wait for a free file descriptor 
  5. dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER); 
  6. fd = open("/etc/services", O_RDONLY); 
  7.   
  8. // Release the file descriptor when done 
  9. close(fd); 
  10. dispatch_semaphore_signal(fd_sema); 

等待queue中的一组任务

Dispatch group用来阻塞一个线程,直到一个或多个任务完成执行。有时候你必须等待任务完成的结果,而后才能继续后面的处理。dispatch group也能够替代线程join。

基本的流程是设置一个组,dispatch任务到queue,而后等待结果。你须要使用 dispatch_group_async 函数,会关联任务到相关的组和queue。使用 dispatch_group_wait 等待一组任务完成。

 
  1. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
  2. dispatch_group_t group = dispatch_group_create(); 
  3.   
  4. // Add a task to the group 
  5. dispatch_group_async(group, queue, ^{ 
  6.    // Some asynchronous work 
  7. }); 
  8.   
  9. // Do some other work while the tasks execute. 
  10.   
  11. // When you cannot make any more forward progress, 
  12. // wait on the group to block the current thread. 
  13. dispatch_group_wait(group, DISPATCH_TIME_FOREVER); 
  14.   
  15. // Release the group when it is no longer needed. 
  16. dispatch_release(group); 

Dispatch Queue和线程安全性

使用Dispatch Queue实现应用并发时,也须要注意线程安全性:

Dispatch queue自己是线程安全的。换句话说,你能够在应用的任意线程中提交任务到dispatch queue,不须要使用锁或其它同步机制。

不要在执行任务代码中调用 dispatch_sync 函数调度相同的queue,这样作会死锁这个queue。若是你须要dispatch到当前queue,须要使用 dispatch_async 函数异步调度

避免在提交到dispatch queue的任务中得到锁,虽然在任务中使用锁是安全的,但在请求锁时,若是锁不可用,可能会彻底阻塞串行queue。相似的,并发queue等待锁也可能阻止其它任务的执行。若是代码须要同步,就使用串行dispatch queue。

虽然能够得到运行任务的底层线程的信息,最好不要这样作。