iOS并发编程指南(4)

Migrating Away from Threads 编程

从现有的线程代码迁移到Grand Central Dispatch和Operation对象有许多方法,尽管可能不是全部线程代码都可以执行迁移,可是迁移可能提高性能,并简化你的代码。 安全

使用dispatch queue和Operaiton queue相比线程拥有许多优势: 网络

应用再也不须要存储线程栈到内存空间 数据结构

消除了建立和配置线程的代码 并发

消除了管理和调度线程工做的代码 app

简化了你要编写的代码 异步

使用Dispatch Queue替代线程 socket

首先考虑应用可能使用线程的几种方式: async

单一任务线程:建立一个线程执行单一任务,任务完成时释放线程 ide

工做线程(Worker):建立一个或多个工做线程执行特定的任务,按期地分配任务给每一个线程

线程池:建立一个通用的线程池,并为每一个线程设置run loop,当你须要执行一个任务时,从池中抓取一个线程,并分配任务给它。若是没有空闲线程可用,任务进入等待队列。

虽然这些看上去是彻底不一样的技术,但实际上只是相同原理的变种。应用都是使用线程来执行某些任务,区别在于管理线程和任务排队的代码。使用dispatch queue和operation queue,你能够消除全部线程、及线程通讯的代码,集中精力编写处理任务的代码。

若是你使用了上面的线程模型,你应该已经很是了解应用须要执行的任务类型,只须要封装任务到Operation对象或Block对象,而后dispatch到适当的queue,就一切搞定!

对于那些不使用锁的任务,你能够直接使用如下方法来进行迁移:

单一任务线程,封装任务到block或operation对象,并提交到并发queue

工做线程,首先你须要肯定使用串行queue仍是并发queue,若是工做线程须要同步特定任务的执行,就应该使用串行queue。若是工做线程只是执行任意任务,任务之间并没有关联,就应该使用并发queue

线程池,封装任务到block或operation对象,并提交到并发queue中执行

固然,上面只是简单的状况。若是任务会争夺共享资源,理想的解决方案固然是消除或最小化共享资源的争夺。若是有办法重构代码,消除任务彼此对共享资源的依赖,这是最理想的。

若是作不到消除共享资源依赖,你仍然可使用queue,由于queue可以提供可预测的代码执行顺序。可预测意味着你不须要锁或其它重量级的同步机制,就能够实现代码的同步执行。

你可使用queue来取代锁执行如下任务:

若是任务必须按特定顺序执行,提交到串行dispatch queue;若是你想使用Operation queue,就使用Operation对象依赖来确保这些对象的执行顺序。

若是你已经使用锁来保护共享资源,建立一个串行queue来执行任务并修改该资源。串行queue能够替换现有的锁,直接做为同步机制使用。

若是你使用线程join来等待后台任务完成,考虑使用dispatch group;也可使用一个 NSBlockOperation 对象,或者Operation对象依赖,一样能够达到group-completion的行为。

若是你使用“生产者-消费者”模型来管理有限资源池,考虑使用 dispatch queue 来简化“生产者-消费者”

若是你使用线程来读取和写入描述符,或者监控文件操做,使用dispatch source

记住queue不是替代线程的万能药!queue提供的异步编程模型适合于延迟可有可无的场合。虽然queue提供配置任务执行优先级的方法,但更高的优先级也不能确保任务必定能在特定时间获得执行。所以线程仍然是实现最小延迟的适当选择,例如音频和视频playback等场合。

消除基于锁的代码

在线程代码中,锁是传统的多个线程之间同步资源的访问机制。可是锁的开销自己比较大,线程还需等待锁的释放。

使用queue替代基于锁的线程代码,消除了锁带来的开销,而且简化了代码编写。你能够将任务放到串行queue,来控制任务对共享资源的访问。queue的开销要远远小于锁,由于将任务放入queue不须要陷入内核来得到mutex

将任务放入queue时,你作的主要决定是同步仍是异步,异步提交任务到queue让当前线程继续运行;同步提交任务则阻塞当前线程,直到任务执行完成。两种机制各有各的用途,不过一般异步优先于同步。

实现异步锁

异步锁能够保护共享资源,而又不阻塞任何修改资源的代码。当代码的部分工做须要修改一个数据结构时,可使用异步锁。使用传统的线程,你的实现方式是:得到共享资源的锁,作必要的修改,释放锁,继续任务的其它部分工做。可是使用dispatch queue,调用代码能够异步修改,无需等待这些修改操做完成。

下面是异步锁实现的一个例子,受保护的资源定义了本身的串行dispatch queue。调用代码提交一个block到这个queue,在block中执行对资源的修改。因为queue串行的执行全部block,对这个资源的修改能够确保按顺序进行;并且因为任务是异步执行的,调用线程不会阻塞。

dispatch_async(obj->serial_queue, ^{

// Critical section

});

同步执行临界区

若是当前代码必须等到指定任务完成,你可使用 dispatch_sync 函数同步的提交任务,这个函数将任务添加到dispatch queue,并阻塞当前线程直到任务完成执行。dispatch queue自己能够是串行或并发queue,你能够根据具体的须要来选择使用。因为 dispatch_sync 函数会阻塞当前线程,你只应该在确实须要的时候才使用。

下面是使用 dispatch_sync 实现临界区的例子:

dispatch_sync(my_queue, ^{

// Critical section

});

若是你已经使用串行queue保护一个共享资源,同步提交到串行queue,并不能比异步提交提供更多的保护。同步提交的惟一理由是,阻止当前代码在临界区完成以前继续执行。若是当前代码不须要等待临界区完成,或者能够简单的提交接下来的任务到相同的串行queue,就应该使用异步提交。

改进循环代码

若是循环每次迭代执行的工做互相独立,能够考虑使用 dispatch_apply 或 dispatch_apply_f 函数来从新实现循环。这两个函数将循环的每次迭代提交到dispatch queue进行处理。结合并发queue使用时,能够并发地执行迭代以提升性能。

dispatch_apply 和 dispatch_apply_f 是同步函数,会阻塞当前线程直到全部循环迭代执行完成。当提交到并发queue时,循环迭代的执行顺序是不肯定的。所以你用来执行循环迭代的Block对象(或函数)必须可重入(reentrant)。

下面例子使用dispatch来替换循环,你传递给 dispatch_apply 或 dispatch_apply_f 的Block或函数必须有一个整数参数,用来标识当前的循环迭代:

queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(count, queue, ^(size_t i) {

printf("%u\n", i);

});

你须要明智地使用这项技术,由于dispatch queue的开销虽然很是小,但仍然存在,你的循环代码必须拥有足够的工做量,才能忽略掉dispatch queue的这些开销。

提高每次循环迭代工做量最简单的办法是striding(跨步),重写block代码执行多个循环迭代。从而减小了 dispatch_apply 函数指定的count值。

int stride = 137;

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(count / stride, queue, ^(size_t idx){

size_t j = idx * stride;

size_t j_stop = j + stride;

do {

printf("%u\n", (unsigned int)j++);

}while (j < j_stop);

});

// 执行剩余的循环迭代

size_t i;

for (i = count - (count % stride); i < count; i++)

printf("%u\n", (unsigned int)i);

若是循环迭代次数很是多,使用stride能够提高性能。

替换线程Join

线程join容许你生成多个线程,而后让当前线程等待全部线程完成。线程建立子线程时指定为joinable,若是父线程在子线程完成以前不能继续处理,就能够join子线程。join会阻塞父线程直到子线程完成任务并退出,这时候父线程能够得到子线程的结果状态,并继续本身的工做。父线程能够一次性join多个子线程。

Dispatch Group提供了相似于线程join的语义,但拥有更多优势。dispatch group可让线程阻塞直到一个或多个任务完成。和线程join不同的是,dispatch goup同时等待全部子任务完成。并且因为dispatch group使用dispatch queue来执行任务,更加高效。

如下步骤可使用dispatch group替换线程join:

使用 dispatch_group_create 函数建立一个新的dispatch group

使用 dispatch_group_async 或 dispatch_group_async_f 函数添加任务到Group,这些是你要等待完成的任务

若是当前线程不能继续处理任何工做,调用 dispatch_group_wait 函数等待这个group,会阻塞当前线程直到group中的全部任务执行完成。

若是你使用Operation对象来实现任务,可使用依赖来实现线程join。不过这时候不是让父线程等待全部任务完成,而是将父代码移到一个Operation对象,而后设置父Operation对象依赖于全部子Operation对象。这样父Operation对象就会等到全部子Operation执行完成后才开始执行。

修改“生产者-消费者”实现

生产者-消费者 模型能够管理有限数量动态生产的资源。生产者生成新资源,消费者等待并消耗这些资源。实现生产者-消费者模型的典型机制是条件或信号量。

使用条件(Condition)时,生产者线程一般以下:

锁住与condition关联的mutex(使用pthread_mutex_lock)

生产资源(或工做)

Signal条件变量,通知有资源(或工做)能够消费(使用pthread_cond_signal)

解锁mutex(使用pthread_mutex_unlock)

对应的消费者线程则以下:

锁住condition关联的mutex(使用pthread_mutex_lock)

设置一个while循环[list=1]

检查是否有资源(或工做)

若是没有资源(或工做),调用pthread_cond_wait阻塞当前线程,直到相应的condition触发

得到生产者提供的资源(或工做)解锁mutex(使用pthread_mutex_unlock)处理资源(或工做)使用dispatch queue,你能够简化生产者-消费者为一个调用:

dispatch_async(queue, ^{

// Process a work item.

});

当生产者有工做须要作时,只须要将工做添加到queue,并让queue去处理该工做。惟一须要肯定的是queue的类型,若是生产者生成的任务须要按特定顺序执行,就使用串行queue;不然使用并发Queue,让系统尽量多地同时执行任务。

替换Semaphore代码

使用信号量能够限制对共享资源的访问,你应该考虑使用dispatch semaphore来替换普通讯号量。传统的信号量须要陷入内核,而dispatch semaphore能够在用户空间快速地测试状态,只有测试失败调用线程须要阻塞时才会陷入内核。这样dispatch semaphore拥有比传统semaphore快得多的性能。二者的行为是一致的。

替换Run-Loop代码

若是你使用run loop来管理一个或多个线程执行的工做,你会发现使用queue来实现和维护任务会简单许多。设置自定义run loop须要同时设置底层线程和run loop自己。run-loop代码则须要设置一个或多个run loop source,并编写回调来处理这些source事件到达。你能够建立一个串行queue,并dispatch任务到queue中,这样一行代码就可以替换原有的run-loop建立代码:

dispatch_queue_t myNewRunLoop = dispatch_queue_create("com.apple.MyQueue", NULL);

因为queue自动执行添加进来的任务,不须要编写额外的代码来管理queue。你也不须要建立和配置线程,更不须要建立或附加任何run-loop source。此外,你能够经过简单地添加任务就能让queue执行其它类型的任务,而run loop要实现这一点,必须修改现有run loop source,或者建立一个新的run loop source。

run loop的一个经常使用配置是处理网络socket异步到达的数据,如今你能够附加dispatch source到须要的queue中,来实现这个行为。dispatch source还能提供更多处理数据的选项,支持更多类型的系统事件处理。

与POSIX线程的兼容性

Grand Central Dispatch管理了任务和运行线程之间的关系,一般你应该避免在任务代码中使用POSIX线程函数,若是必定要使用,请当心。

应用不能删除或mutate不是本身建立的数据结构。使用dispatch queue执行的block对象不能调用如下函数:

pthread_detach

pthread_cancel

pthread_join

pthread_kill

pthread_exit

任务运行时修改线程状态是能够的,但你必须还原线程原来的状态。只要你记得还原线程的状态,下面函数是安全的:

pthread_setcancelstate

pthread_setcanceltype

pthread_setschedparam

pthread_sigmask

pthread_setspecific

特定block的执行线程能够在屡次调用间会发生变化,所以应用不该该依赖于如下函数返回的信息:

pthread_self

pthread_getschedparam

pthread_get_stacksize_np

pthread_get_stackaddr_np

pthread_mach_thread_np

pthread_from_mach_thread_np

pthread_getspecific

Block必须捕获和禁止任何语言级的异常,Block执行期间的其它错误也应该由block处理,或者通知应用

相关文章
相关标签/搜索