现在的位置: 首页 > 综合 > 正文

iOS并发编程指南(2)

2013年10月16日 ⁄ 综合 ⁄ 共 5911字 ⁄ 字号 评论关闭

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的同步也比基于锁的线程同步机制更加高效。

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

dispatch queues的几个关键点:

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

系统决定了同时能够执行的任务数量,应用在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.     MyD
【上篇】
【下篇】

抱歉!评论已关闭.