并发编程指南

并发

1 概述

1.1 并发的概念

并发其实指的是多项任务在同一时间进行,随着多核CPU的普及以及发现任务只在某一核上不停的增加,软件开发者需要一种方式充分的利用多核系统。虽然诸如IOS、MacOS兼容多项程序同时执行,但是大多数的程序运行在后台,并且执行任务只需要很短的CPU时间。这是因为前台应用同时获取了用户的关注并且是设备处于忙的状态。如果一个程序有很多的任务需要处理,但是又只获得了很少的碎片CPU时间,那些额外的处理资源将被浪费。 在过去,在应用中引入并发要求创建一个或者多个额外的线程,遗憾的是,编写线程代码是一项非常有挑战性的事情,线程是一种‘低级’的工具,并且需要手动去管理,获取多少个线程是最优的,动态取决于当前的系统以及底层硬件的支持。实现一个完美的线程解决方案,变的极其困难或者说不可能实现。另外,采取添加线程的方式实现同步的机制,不仅给软件开发带来了复杂性和风险性,而且也并不能确保提升性能。 OSX和iOS采取了一种比传统的基于线程的系统和程序,更加异步的方式去执行并发任务。并不是直接去创建线程,应用需要做的是定义一些特殊的任务,然后让系统去执行它们,让操作系统去管理线程,程序获得了原生线程更强的伸缩性,而程序开发者也获得了更加简单更加高效的编程模型。 本文档就是描述这项技术和技艺,你应该使用这项技术来实现并发。这项技术同时使用于iOS和OSX。

1.2 文档的组织

本文档包括以下章节
1、并发和程序设计——介绍异步程序设计的一些基本概念和异步的执行自定义任务的技术
2、操作队列——展示怎样用OC去组装和执行一个任务
3、分发队列——展示怎样在基于C的应用程序中去并发地执行任务
4、资源分发——展示如何异步的处理系统事件
5、从线程变成用迁移到新的技术——提供一些技术帮助从旧的线程变成迁移到新的变成技术 本文当同时也包括一些相关章节的专业术语的定义

1.3 一些专业名词

在开始讨论并发之前,非常有必要去定义一些相关的专业名词来避免混淆,一些开发UNIX或者早先开发OSX的程序员,可能会对任务,队列处理、线程有一些新的任务,本文档对这几个概念有如下定义
1、在本文中,线程专指一个分离的支线去执行一段代码,而在老的OSX中特指基于POSIX的API 2、在本文中,执行专指可执行的任务正在执行中,可能包裹着多个线程。
3、在本文中,任务专指一个需要被执行的抽象工作 关于完整的类似这些概念的定义,参见词汇定义

1.4 其他

本文档专注于如何在你的程序中实现并发编程,而不包括如何使用线程,如果你需要更多的有关线程开发的只是,请去参考线程编程指导

2 并发及程序设计

2.1 并发编程和程序设计

在以前的计算机运行的时候,计算机单位时间最大执行数量的任务取决于CPU的时钟速度,但是随着技术的发展和中央处理器设计的更加紧凑,热量和物理因素开始限制中央处理器的最大时钟速度,所以,主板供应商也在寻找其他的方式去提升他们主板的总体性能,他们发现的解决方案是增加主板上的处理器个数,通过增加处理器个数,一个主板可以在单位时间内执行更多的任务,而并不需要去增加CPU的时钟速度,或是改变主板的大小或考虑热量参数,现在剩下的唯一问题就是如何去充分的利用这些多核。 为了利用这些多核,计算机需要程序设计者能够去同时执行多项任务,在当代,多核操作系统,如OSX或者IOS,可能有几百或者更多的程序在同时运行,所以根据时序安排程序在不同的中央处理器上变得成为可能,然而,大多数的时候,这些程序要么是系统守护进程,要么是那些消费很小处理时间的后台应用程序,取而代之的是,真正需要多核开发的是,独立的前台运行的程序更高效的获取更多核心。 传统的使应用获取多核的方式是创建多线程,然而,随着多核的增加,线程方案有一些问题,最大的问题是线程开发的方式对多核系统不具备很好的伸缩性,你不能仅靠创建更多的行程就能确保程序和处理器运行良好,你需要知道的是,如何高效的利用这些中央处理器。对于程序来讲,如何计算它自身是一件有挑战的事情,尽管你可以正确的管理这些线程,程序去管理这些线程也依然是一项挑战,去确保他们高效运行、确保他们不被别的线程干扰。 所以,总结这些问题,程序需要一种方式去充分应用多核,单个程序可伸缩的执行大量工作,而且这个解决方案需要足够简单,去面对单个处理器任务的任务增长,好消息是,苹果已经提供了解决方案对处理所有这些问题,本章节先睹为快,去看看这项技术的组成以及牛逼的设计,你可以使你的代码从中获益。

2.2 从线程开发中走出来

尽管线程开发已经应用了多年,而且它们在某些地方还将有用武之地,但是它们并没有可伸缩的解决多核场景,如果你使用线程开发,那么创建一个可伸缩的解决方案的麻烦就落在你肩上了,开发者,你需要去动态的根据系统的多核个数去决定创建多少个线程,此外,你的应用将花费很多消耗在创建和管理这些线程本身的消耗上。 取代线程开发,OSX和IOS使用异步设计的方式解决并发问题,异步方法已经在操作系统中提供了很多年,而且一般都是应用在创建很耗时的任务上,如从磁盘读取文件。当调用的时候,一个异步任务开始在后台执行,并立即返回在任务执行完之前。通常,这项任务会调用一个后台线程,开始这项任务在该后台线程上,然后在任务完成的时候发送一个消息给调用者(通常使用回调的方式)。在过去,如果没有一个你需要的异步方法,你需要自己去写一个异步方法还需要创建自己的线程,但是现在,OSX和iOS提供技术,你可以不用自己去管理线程就可以异步执行任务。 一种开启异步任务的技术叫 GCD,一项将过去需要在你程序中自己写管理线程代码移交给系统去管理。所有你需要做的事情仅仅是创建任务,然后将任务提交给合适的系统的GCD队列。GCD去创建和管理这些线程,因为这些线程已经提交给系统去管理,GCD提供全套的任务管理和执行,比传统的线程管理更加高效。 操作队列是非常类似于分发队列的一种OC对象管理方式,你可以定义你想执行的任务,然后把他们扔到操作队列中,当执行这些安排好的任务时,类似于GCD,操作队列为你执行所有的管理。确保在系统长执行的高效且迅速。 下面的段落提供了更多的信息关于操作队列、分发队列、以及一些你可能在异步编程中用到的技术

2.2.1 分发队列

分发队列是一套C的机制,为可执行自定义任务。分发队列要么是串行的要么是并行的,但是通常都是按照FIFO的方式去调度任务,一个串行的队列,一次只能运行一个任务,只有等前一个任务完成之后,下个任务才能开启。作为对比,并行的队列可以同时开启尽可能多的任务而并不需要前面的任务执行完毕。 分发队列有下面几个好处

1、提供了简单的直观的编程接口 
2、提供了自动的和完全的线程池管理 
3、任务执行速度提升 
4、更多的内存方面的优化 
5、他们不会增加内核的负载 
6、不会导致队列死锁 
7、伸缩性强(对多核系统来讲) 
8、串行队列提供了比过去线程同步更为优秀的一种选择 

你提供给分发队列的任务必须是封装好的一个方法或者是一个Block对象,Block是一种具有C语言特性的,开始引进与OSX 10.6,IOS 4.0的一个新特性。但是相比C语言有一些别的好处。不同于在Block的语法区域定义Block对象,你通常在别的方法或者是函数中去定义,这样可以去捕获到别的方法或者函数中的变量,Block同样可以移动到他们的作用区域以外,拷贝到堆上,这通常发生在你将任务添加到一个分发队列上的时候,所有这些语法特性,使得通过添加少量代码就可以获得非常好的实现。 分发队列是GCD技术的一部分,也是C运行时的一部分,要想获取更多的有关分发队列相关的信息,请参见 分发队列,要获取更多关于Block的信息和他们的好处,参见Block 程序编程观点。

2.2.2 分发资源

分发资源是异步的处理系统分发资源的一种C语言机制,分发资源封装了一个特殊类型的系统事件,并将这个特殊的系统事件提交给一个特殊的Block对象,或者函数,当系统事件发生的时候,你可以使用分发资源去监测如下特殊系统事件 定时器 信号事件 描述符相关事件 进程相关事件 端口匹配相关事件 自定义的事件,并由你来触发 分发资源也是GCD技术的一部分,要获取更多有关分发资源相关的信息,参见分发资源

2.2.3 操作队列

操作队列是cocoa环境的一种并发队列,由NSOperationQueueClass 实现,操作队列总是按照FIFO的方式去执行任务,操作队列考虑其他因素去影响执行队列的顺序,在这些因素中,优先考虑一个任务的执行是否是依赖于其他任务,你可以给自己的任务设置依赖关系,然后创建一个复杂的执行顺序图。 任务添加到操作队列中的,必须是NSOperation的子类,一个操作对象是一个OC类型的封装了你要执行的数据和任务的对象,由于NSOperation是一个抽象基类,所以你通常需要去自定义子类去执行你的任务,然而,Foundation Framework已经提供了一些相关的子类,你可以使用它们去执行任务。 操作的对象产生KVO通知,这个是非常有用的,当监视你的任务进度的时候,虽然任务执行通常是并发的,但是可以利用依赖是的任务有序。 关于更多操作队列的信息,参见操作队列

2.3 异步编程技术

在你开始考虑重新用并发编程的方式重新设计程序的时候,你最好先问下自己这么做是否必要,并发可以提高你代码的可响应性,去确保主线程能相应更多的用户事件,它同样可以提高你代码的性能,通过促进多核去执行更多的任务,但是它也同时带来了上层复杂性,从而使你的代码更加难调试。 由于它带来了复杂性,并发并不是一个在你程序开周期中考虑的特性,当把它嫁接到应用中。做对这件事需要你好好考虑你程序执行的任务以及向这些任务提交的数据结构。如果做错的话,你可能发现你的代码运行缓慢,甚至还不如从前,因此,在你开始程序设计的时候,你就应该考虑你要实现的目标和通过何种方式来达到它。 每个应用都包含有不同的要求和不同的任务需要它去执行,这不可能靠一个文档就告诉你,怎么去设计你的程序和管理任务,然而,下面几段可以给你提供一些指导,帮助你去在程序设计的时候做出好的选择。

2.3.1 明确程序期望的表现

在你决定是否要将并发引导到应用中的时候,你应当开始思考你的应用程序想要达到的一个什么表现。明白了应用要达到的表现之后,会给你是否使用并发提供一个参考。同样的,也会给你一些引入并发之后程序能获取的性能收益方面的启发。 首先、你需要列举应用程序所要执行的任务和数据结构之间的关系。开始,你可能通过点击一个菜单或者一个按钮开启一个任务并执行,这些任务可能是一些离散的任务,并有明确的开始和结束点。你还需要列举出应用程序可能执行的其他类型的任务,而不仅仅是用户行为相关的,比如说基于时间的一些任务 在你有了自己任务列表之后,开始把任务进行更加的分组集合,确保这些任务能够成功执行。在这个层面,你优先考虑的是那些数据或对象修改如何对应用状态进行修改的。你同样需要考虑不同任务之间的相互依赖关系,例如:如果一个任务牵涉到一个数组中所有对象的修改,对于其中一个数据的修改,会对其他数据产生任何影响。如果一个数据的修改,独立于其他的数据,那么这个时候你可以考虑使用并发去做提升性能,创建多个任务去做。

2.3.2 单位工作的可执行因子

在明白你程序执行的任务类型之后,你应该明白在什么地方去使用并发会有好处了。如果在一个任务中改变一个或者多个的顺序,会影响到执行结果。你应该还是需要考虑到使用串行的方式去执行这些任务,如果改变执行顺序之后,并不会影响到执行结果,你可以考虑将这些任务用并发的方式去做,在这俩种情形下,你定义可执行的单位工作,并让它们执行,这些工作单元,就成为你封装好的Block对象或者操作对象或者分发队列。 对于每个单独的可执行任务,并不需要过多的担心任务执行的数量,在最后,分到线程中总是会有开销,但是分发队列或者操作队列相比传统的线程开发还是有很多优势的,因此,执行一些单元工作使用操作队列还是要比直接操作线程要好很多,当然,你常常应该确保任务执行的性能和你开启的任务恰好如你所需,但是,任务并不是越小就越好。

2.3.3 区分你需要什么队列

此时,你的任务已经切割为一些可执行的单元,并且封装成了Block对象或者是操作对象,你需要去定义你要执行的队列以执行这些任务。对一个任务来讲,测试这些Block或者是操作任务,能够在队列中执行正确。 如果你使用Block去实现你的任务,你可以添加任务到串行或者并行的队列里,如果对顺序有要求的话,你只能添加到一个串行队列中,如果没有要求,根据你的要求,你可以添加的并行队列中,或者添加到多个队列中去。 如果你是用操作队列去执行任务,那么选择的队列并不对添加到里面的任务的配置感兴趣,如果要串行去执行任务,那么你需要给相关的任务设置依赖关系,依赖会阻止任务开始执行,直到它依赖的任务执行完成。

2.3.4 提升执行效率的贴士

在把任务分割为更小的任务并将他们添加到队列中后,这里还有一些使用队列提升程序性能的小Tips 如果内存考虑是一个因素的话,那么在任务里面直接计算。如果你的应用已经内存警告了,那么在任务中直接计算会比从内存中加载要快一些。运用在寄存器或者是该核上的内存计算,会比从主内存加载要快一些。当然,你也可以通过测试来选择哪种方案会好一些。 将串行的任务,可能的话改变为并发。如果一个任务由于共享资源必须串行去执行,可以考虑将共享资源移除使得可以并发执行,可以考虑把这些资源给每个客户都拷贝一份。 避免使用锁,操作队列和分发队列的支持在大多数情况下并不需要锁。做为取代,可以使用一个串行的队列或者使用依赖去保证顺序正确。 如果可能的话依赖系统框架,最好的方式去实现并发就是使用系统提供的框架,许多框架使用线程或者其他技术来实现并发,当定义你的任务的时候,看看系统框架中是否已经有方法或者函数能够实现并发,使用系统接口会提升你的效率,而且帮你做到更多的并发可能性。

2.4 性能相关

操作队列、分发队列、资源分发提供了一种执行并发更加容易的方式。但是这些技术并不保证提升程序的性能和可响应性。具体来讲,根据你的需要同时兼顾性能提升和不要影响到其他资源仍然是你自己需要衡量的一件事。举个例子来讲,尽管你创建了10000个任务并把他们都提交到操作队列中,这么干的话,肯定会导致你的程序分配潜在的大量内存,这个会导致增加调度和减少性能。 引入并发到你的程序中前–是使用队列还是线程,你需要根据程序当前的性能来设置一个参考的标准。在引入并发之后,也同样需要做个列表对性能方面的改变进行比较,来确保程序是真的性能提升了,如果引入并发并不能给性能带来大的提升,你应该考虑其他的性能工具来检测潜在的原因。 关于性能的介绍和可用的性能工具,参见性能概览。

2.5 并发和其他技术

将你的代码分解为模块任务是一种最好的提升应用并发的方式,但是这种设计方式并不是对每个应用每个情况都很适合,取决于你执行的任务,也可能存在其他的选择来提升程序的整体并发,下面的俩小节提供了其他的并发技术供你参考

2.5.1 OpenGL和并发

在OSX中,OpenGC技术是图形计算方便最基础和核心的技术,OpenGL是非常棒的计算大数据集合的一种技术,举个例子来讲,你可能使用OpenGL技术来给图片做像素级别的滤镜,或者用它来计算复杂的数学计算。换而言之,OpenGL可以用来计算大量数据集合是并行的。 尽管OpenGL在大量数据并行执行方面有着非常好的性能优势,但是它并不合适执行自定义的计算,在任务提交给GPU处理之前,有大量的准备工做,而且要将数据和其他必要的核心操作转换图形卡片。同样的,要想获取OpenGL的产生结果也需要偶很多其他的努力。因此,所有跟系统交互相关的任务不建议提交给OpenGL去执行,举个例子,你不应该用OpenGL去运算从文件中或者网络流中拿到的数据,取而代之的是,你要用OpenGL执行的任务,必然是自己已经持有的这样相对来讲传递给GPU运算更加独立。 更多关于OpenGL的只是,参见OpenGL开发指导。

2.5.2 何时使用线程

尽管操作队列和分发队列是执行并发任务的更优选择,但是它们不是万能的,取决于你的应用程序,这里仍然后一些情况是需要创建线程去开发的,如果你创建了自定义的线程,那么你要确保尽量少的开启线程,并且确保这些线程只执行特定的任务,而不去干别的事情。 线程仍然是一种较好的解决方案,当执行实时任务的时候,分发队列会确保尽快的去执行他们的任务,但是并不能实时的去开线程去做。如果你需要可预测的后台代码执行,那么线程仍是一种选择。 作为线程编程,你还是需要去使用线程,当必须和完全必要的时候,关于更多线程方面的知识,参见线程开发指导。

3 操作队列

cocoa的操作都是原生的对象封装的可执行异步任务。这些操作同时可提交给操作队列去执行,也可以直接自己执行。由于是基于OC的框架,那么操作可应用在IOS和OSX中。本章节将介绍如何去使用和定义操作。

3.1 关于操作对象

一个操作对象是一个NSOperation类的实例,你可以用它来封装你要执行的任务。NSOperation类本身是一个抽象基类。所以要想执行可用的任务,必须得使用它的子类。尽管是一个抽象类,它还是提供了一些有用的基本操作,而省去了你在自己定义的子类中去做的麻烦。此外,Foundation 框架还提供了俩个具体的子类,可以用它们来执行你的任务,下表列出了这俩个类,还有如何使用它们的介绍。 NSInvocationOperation :这是一个基于你的类和要执行的Seletor的类,你可以在已经定义过这个任务的地方去调用它,然后执行异步操作。由于它并不要强制你去做继承,所以可以使用该类去实现一个更加动态的风格。 NSBlockOperation:这是一个执行block封装任务的操作类,因为它可以执行多个block。block任务执行任务使用了组的语法,当所有的相关的block执行完之后,这个block操作才被任务是执行完成 NSOperation:这是定义其他操作任务的基类,继承它给你自定义操作类带来了完成的并发控制以及实现。包含可以完全控制任务的执行能力和执行状态。 所有上述的操作对象都包含以下核心的特性 支持建立图形化的任务间相互依赖,这些依赖将会阻止任务开始,直到它依赖的任务完成之后。 支持可选的完成回调block。指的是当所有的执行任务结束之后的回调方法。 支持检测任务执行状态(使用KVO) 支持对操作顺序进行调整,并影响他们的执行顺序。 支持取消语义,也即允许你半路对任务执行终止操作。 操作对象是设计来帮助你提升程序的并发水平,操作也是一种不错的组织和包装你的应用为几个独立的分支的一种手段,作为取代提交一些任务给主线程的开发方式,可以将任务分割为一个或多个不同的操作,然后提交给队列,使相关的工作可以在一个或多个线程中去并发的执行。

3.2 同时VS非同时操作

尽管,你通常将任务添加到操作队列中,但是做这个并不是必须的。你也可以直接调用它的start方法去开始一个操作。但是执行这个操作,并不能保证你的操作同步的运行在你当前代码所在的线程中。那么NSOperation类的这个isConcurrent方法会告知你,当前运行的操作是同步还是异步的在你调用Start方法的线程中,默认情况下这个方法会返回NO,也就是说这个操作是在当前调用的线程中同步去执行的。 如果你想去实现一个异步执行的操作,也就是调度任务的线程和任务执行的线程是异步的。你需要写额外的代码去异步的开启它。举个例子,你可以创建一个独立的线程,调用一个系统的异步方法,去执行其他的事情以保证start函数的调用时异步去执行。 大多数的开发并不需要去实现一个并发操作对象,如果你总是将你的操作去添加到一个操作队列中,你并不需要实现这些并发操作,当你添加一个操作对象到操作队列中的时候,操作队列自己就会创建一个线程去执行你的操作,因此,将一个并不是异步的任务添加到操作队列中去,结果还是会以异步的方式去执行代码,去定义一个异步任务这种费力不讨好的工作,也只是你就是想创建异步执行任务,而不是将它添加到队列中去的时候才有必要。 关于更多有关异步任务的信息,参见设置任务去异步执行。

3.3 创建一个NSInocationOperation对象

NSInvocationOperation是NSOperation的具体子类,当它运行的时候,会执行它的selector里面,指派给它的任务。使用该类时,应该避免将大量的自定义的操作给每个任务。尤其是当你需要改变一个已经存在的应用程序和这些对象已经存在任务,还有很多必备要执行的任务时。你可以使用它来改变依赖环境的时候。举个例子,你可以使用一个Invocation对象来执行一个选择器,这个选择器是基于获取用户的输入信息动态的选择。 创建一个Invocation操作的步骤是很简单的,你可以创建并实例化一个该对象,然后传递需要的对象和selector去执行指定的代码,下面提供了俩个方法去举例说明这个操作步骤,taskWithData:方法创建一个操作对象,然后通过另外一个方法去执行任务。

1
2
3
4
5
6
7
8
9
10
11
12

@implementation MyCustomClass

- (NSOperation*)taskWithData:(id)data {
NSInvocationOperation* theOp = [[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(myTaskMethod:)
object:data];
return theOp;
} // This is the method that does the actual work of the task.

- (void)myTaskMethod:(id)data { // Perform the task. }
@end

3.4 创建一个NSBlockOperation对象

NSBlockOperation是NSOperation的具体子类,作为一个或者多个Block对象的封装器,这个类提供了对象层面的封装,而且它已经使用了操作分发队列,所以并不需要创建分发队列,但是你也可以使用其他的诸如操作任务的依赖、KVO通知或者其他特性,而这些特性是分发队列所没有的。 当你创建一个block操作的时候,在创建他的时候,你至少已经添加了一个block,你也可以在之后再添加更多的block进去。当执行NSBlockOperation对象的时间来临的时候,该对象会将它的所有block都提交给默认优先级的异步分发队列,然后这个对象等待它的block全部执行完,之后会把自己标记为isfinish。因此,你可以使用一个Block操作去监听一组任务的完成,非常像用一个线程去管理多个线程的结果。而不同之处在于block操作自己运行在一个分离的线程,而你的程序可以干其他的活儿,在这个block等待它的任务执行完成之前。 下面的代码介绍了如何去创建一个BlockOperation,这个Block没带参数且没有返回什么有意义的结果 NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{ NSLog(@”Beginning operation.\n”); // Do some work. }]; 当创建一个block操作之后,你可以添加更多的block给它,通过 addExecutionBlock:方法,如果你想让添加的任务线性去执行,那么你必须直接将它提交给指定的串行队列中去。

3.5 定义一个自定义的操作

如果一个Block操作对象或者Invocation操作对象没法完全满足你的需求的时候,你可以考虑自己定义个继承自NSOperation的操作对象,NSOperation对象提供了一系列的继承点给所有的操作对象,这个类已经实现了大量的基础的函数或方法,满足那些依赖或者KVO的操作,然而,还是有一些地方需要你自己的自定义实现确保你的操作对象能够正确的执行,具体的工作量的大小取决于你要自定义的是一个同步操作对象还是异步操作对象。 定义个同步操作对象要比定义一个异步操作对象简单的多,对于一个同步操作对象来讲,所有你要做的工作就是实现main执行函数和响应取消操作事件。父类已经帮你做了所有其他该做的工作,而对于一个异步操作对象来将,你需要替换一些父类已经做过的工作,在你自己定义的操作对象上。下面的俩小节将介绍如何去实现这俩种不同的操作对象。

3.5.1 执行main任务

至少一个操作对象需要实现下面的函数 一个自定义的初始化函数 main 你需要自定义个初始化函数去初始化一个操作对象,以及一个自定义的main函数去执行任务,你也可以根据需要执行其他的函数,如下所示 自定义函数供你的main函数去调用 属性方法去访问数据 实现NSCoding协议去固化一个操作对象 下面的模板,展示了一个自定义的操作对象,下面的代码并不展示如何去实现取消方法,但是也实现了你通常要实现的方法,具体怎么取消任务,参见取消任务介绍,下面的初始化函数初始化了一个对象,携带一个数据参数并把它存到了类内部方便以后访问,main函数将会显式的去操作这个对象,在你的应用将结果返回给你之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface MyNonConcurrentOperation : NSOperation
@property id (strong) myData;
-(id)initWithData:(id)data;
@end
@implementation MyNonConcurrentOperation
- (id)initWithData:(id)data {
if(self = [super init])
myData = data;
return self;
}

-(void)main {
@try {
// Do some work on myData and report the results.
}
@catch(...)
{
// Do not rethrow exceptions.
}
}
@end 更多的细节参见NSOperationSample

3.5.2 响应取消事件

当一个操作开始执行的时候,它将持续执行任务直到结束或者任务被显式的取消掉,取消操作可能发生在任何时候,甚至是操作开始执行的时候,尽管NSOperation对象提供了一个方式供子类去使用,指出取消事件是完全必要的。如果一个操作完全结束了,那么也可以提供一个方式去清理之前分配的资源等等,所以,一个操作对象需要去检测是否已经取消了,然后就可以优雅的结束任务。 操作对象支持取消操作,你应该做的事情就是频繁的去检查是否任务已经被取消了,支持取消是非常重要的,对于你自定义的任务和系统给的那俩个子类操作对象来说,isCancel方法是非常轻量级的,可以频繁去访问而不会造成内存方面的空扰,当设计一个操作对象的时候,你可以在如下几个地方去访问iscancel 1、在你开始执行任务之前 2、在开启一个loop之前,或者是更加频繁的在每个循环之前 3、在你的每个可能导致任务退出的地方 下面的代码介绍了在main函数中如何去调用cancel,在这个例子中,iscancel在每次while loop前都会调用,使得任务可以快速的退出,且获取了一个定期的间隔。

1
2
3
4
5
- (void)main { 
@try { BOOL isDone = NO;
while (![self isCancelled] && !isDone)
{ // Do some work and set isDone to YES when finished } }
@catch(...) { // Do not rethrow exceptions. } }

尽管上面的代码,并不包含清理数据的工作,但是你自己的代码还是要保证资源被及时的释放。

3.5.3 定义一个可并发执行的操作

一个操作对象默认情况是按照同步的方式去执行任务,也就是说他们执行任务的线程也就是start调用的线程。因为操作队列会对操作任务提供开启的线程,因此,大多数的任务就会异步运行,然而,如果你计划手动去执行一个任务,而且还希望这个任务异步的去运行,你必须得采取一些手段来保证能够这么干,你需要把你的操作对象定义为一个可并发执行的操作对象。下面列出来的函数就是你需要重写的并发操作。 start:(必须重写)所有的自定义并发操作必须重写这个函数,从而替换之前这个函数的默认实现。要手动的执行一个操作,你就可以调用start函数,因此从,你对该方法的实现就是自定义操作对象的开启任务的节点,也就是你要提交你的任务到线程中去执行的节点,你的实现在任何时候都不应该去调用super start。 main:(可选的)这个函数通常用来实现与操作对象相关的任务。虽然你可以把执行任务的任务放到start中去执行,利用mian方法去执行任务会对你的任务开始和清理工作有好处。 isExecuting:(必须的) isFinish:(必须) 并发操作非常有必要向使用它的客户报告配置环境和执行状态,因此一个并发操作必须得包含执行的状态信息包括何时开始执行任务,何时结束任务,如果要报告状态,那么必须使用这些方法。 当别的线程在同时调用上述方法的时候,你必须得保证这些方法是安全的。同时,你也必须得实现KVO通知,以报告这些状态。 isConcurrent:(必须)区分一个操作是否是并发操作,重写该方法,并返回YES 本小节的剩余部分将展示一个MyOperationClass的例子,这个类列举了实现一个并发操作的基本函数,MyOperation 类将在它自己开启的线程中执行任务,而正在的执行任务将与操作类是不相干的,下面例子将的几点,就是你在定义一个并发操作时需要提供的一些基础函数。 下面的代码显示了部分MyOperation的接口和实现函数,这些实现包括isConcurrent、isExecuting,isFinish,等函数。其中isConcurrent函数非常简单,只需要返回YES去指明本操作是一个并发操作就可以了,isExecuting 和 isFinished 也比较简单,返回在类中存储的成员变量的值就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface MyOperation : NSOperation 
{
BOOL executing;
BOOL finished;
}
- (void)completeOperation;
@end
@implementation MyOperation
- (id)init {
self = [super init];
if (self) {
executing = NO;
finished = NO;
}
return self;
}
- (BOOL)isConcurrent { return YES; }
- (BOOL)isExecuting { return executing; }
- (BOOL)isFinished { return finished; }
@end

下面的方法展示了MyOperation的 start函数,下面的实现是一个start函数,执行任务至少要实现的内容。在这种情况下,start函数简单的开启一个新的线程,然后让这个线程去调用main函数。这个函数同样要更新executing成员变量,而且要对isExecuting关键路径产生KVO通知,当这些工作完成之后,就会离开函数,并去到新的线程中去执行任务。

1
2
3
4
5
6
7
8
- (void)start { // Always check for cancellation before launching the task. 
if ([self isCancelled]) { // Must move the operation to the finished state if it is canceled. [self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"]; return; }
// If the operation is not canceled, begin executing the task.
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil]; executing = YES; [self didChangeValueForKey:@"isExecuting"];
}

下面展示了MyOperation剩下的实现,在上面的代码已经知道 main函数将作为新的线程开启任务的节点,它将执行操作对象带来的任务,并且要执行completeOperation函数来说明任务已经结束,completeoperation函数将对isExecuting和isFinished关键路径发送KVO通知,来反应操作对象的状态已经改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)main { 
@try {
// Do the main work of the operation here.
[self completeOperation];
}
@catch(...) {
// Do not rethrow exceptions.
}
}
- (void)completeOperation {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}

尽管是操作对象被取消掉了,你也应该发送KVO通知,来通知你的任务已经完成,当一个操作对象的执行时依赖其他操作对象的时候,它将检测isFinished状态,只有当它依赖的所有isfinished状态都变为YES的时候,如果无法发送isFinshed通知将会阻止其他依赖他的操作对象的执行。

3.5.4 维护KVO的兼容

NSOperation类是服从下列关键字的KVO isCanceled isConcurrent isExecuting isFinished isReady dependencies queuePriority completeBlock 如果你重写了start函数,和自定义了一些其他比较重要的函数,而不是单单重写main函数,你必须保证自定义的对象能够响应KVO,当重写start函数,其中你最需要关注的就是isExecuting和isFInished,这里有大量的方法会受到这俩个函数的影响。 如果你想实现支持依赖关系,以对其他一些操作对象的时候,你也可以重写isReady函数,并强制返回NO,直到你自定义的依赖关系满足。(如果你仍然想支持系统提供的依赖关系的时候,请保证调用super的函数)当你操作对象的isReady状态发生变化的时候,请发送KVO通知去报告这个状态,不过幸运的是,除非你去重写addDependency和removeDependency:,你并不需要担心KVO通知的发送。 虽然你可以发送其他关键字的KVO通知,但是通常你并不需要这么做,如果你想取消一个任务,你可以简单的调用cancel函数就好了。同样的,你也不需要去修改队列的优先级在操作对象上,最后,除非你的操作对象支持动态的修改isConcurrent,你也不需要发送KVO通知给isConcurrent关键路径。 如果要知道更多关于KVO的操作,请参见KVO编程指导。

3.6 自定义一个操作对象的执行行为

在添加一个操作对象到操作队列里面之前,需要将一个操作对象设置好,这些对操作对象设置的信息将会对所有的操作对象有效,不仅是自定义的操作对象,还包括系统定义的那俩个操作对象。

3.6.1 设置依赖关系

依赖是一种限制操作对象执行顺序的一种手段,一个操作对象的执行,必须得在它依赖的所有对象执行完成之后才能进行。也就是说,你可以创建简单的一对一的或者是负责的依赖关系树。 在俩个操作对象之间创建依赖关系,你可以使用addDependency:方法,这个方法可以在你传的目标对象和自己之间创建一个依赖关系。也就是说在目标对象没有执行完成之前,你的这个对象是不会执行的。依赖还不仅仅限制在一个操作队列中,因为是操作对象管理依赖关系,所以在不同的操作队列之中,依赖也是可以起到作用的。但是有一种情况是不允许的,那就是设置依赖环,这是一种语法错误,会导致任务永远得不到执行。 当操作对象所依赖的所有操作对象都执行完成之后,操作对象就变成ready状态,准备执行。(如果是你自己定义的操作对象 ,并自定义了isReady函数,那么准备状态就和你设置的条件有关系了)如果一个操作对象是在操作队列中,那么isReady状态的操作对象在任何时候都可能被执行,如果你计划手动去执行一个操作对象的话,那么可以调用他的start方法。 重要:你应当在将操作对象提交到操作队列之前就去设置依赖关系,如果在之后去设置依赖,也许将不会阻止该任务的执行。 依赖基于的是操作对象间在任何时候都可能发生变化的KVO通知,如果你自定义了操作对象的话,你需要自己去发送KVO通知,以防出现依赖方面的问题。关于更多KVO的信息,参考维护KVO兼容,关于更多设置依赖的信息,参考NSOperation 类

3.6.2 改变操作对象的执行优先级

对于添加到队列中的操作对象,是否能够执行,首先是受操作对象的isReady状态控制,其次是他们的优先级,是否准备好执行,是由操作对象的依赖对象是否执行完来限制的,但是这个优先级是操作对象的一个属性值,默认情况下,所有新创建的操作对象都是普通优先级,但是你可以增加或者是减少操作对象的优先级,通过setQueuePriority方法 优先级只对同一个队列中的操作对象起作用,如果你的应用有多个操作队列,不同队列中的操作对象的优先级是相互独立的,也就是说,在不同队列里面,存在低优先级的操作比高优先级的操作先执行。 优先级并不是依赖的替代,优先级决定的是那些在队列中已经是准备状态的操作对象的执行顺序,举个例子,如果一个队列中同时有高优先级和低优先级的操作对象准备好了,那么操作队列先执行高优先级的操作对象,然而,如果高优先级的没有准备好,但是低优先级的准备好了,那么将先执行低优先级的,如果你想阻止一个操作任务在另外一个操作对象执行完之后再执行,你需要使用依赖关系去 做

3.6.3 改变依附线程的优先级

在OSX 10.6及以后,设置一个操作对象所在的线程的优先级成为可能,系统的线程策略是依靠内核去管理,但是高优先级的线程将获得更高的执行机会,对一个操作对象,你可以显式的指定它的线程优先级,通过设置一个浮点型的数值,0-1.0之前。0是优先级最低、1是优先级最高。如果不显式设置的话,系统默认的线程优先级是0.5. 要设置线程优先级的话,你需要在讲操作对象添加到操作队列之前,调用setThreadPriority函数。当它的执行时间到来的时候,默认的start函数就会设置你之前指定的线程优先级来修改优先级。这个优先级只在你执行的main函数执行过程用有效。所有的其他代码包括你的完成回调,仍然是运行在默认的优先级下的。如果你自定义了一个并发的操作,那么你需要重写start函数,并手动去修改线程的优先级。

3.6.4 设置一个完成后的回调block

在OSX 10.6及以后,一个操作对象可以在它的所有任务执行完成之后去执行一个完成的block,你可以用这个完成的block执行任何与main函数执行的任务里面不相关的的block。举个例子,你可能需要告诉客户这个操作对象的任务都执行完成了,一个并发的操作对象可能会使用这个block去执行它最后的KVO通知 要设置一个完成block,你可以使用setCompleteBlock:函数,这个函数不需要传任何参数,也没有返回值。

3.7 关于实现一个操作对象的一些Tips

尽管一个操作对象可能非常容易去实现,但是有一些事情在你自定义的操作对象上还是要注意一下,下面的小段描述了这么几个方面。

3.7.1 管理操作对象的内存

下面的小段,描述了几个在操作对象中管理内存的关键元素,关于更多的OC的内存管理,参见内存管理开发指导。

3.7.1.1 避免依靠线程存储数据

尽管大多是的操作都是在一个线程中执行的,在同步操作对象中,这个线程通常是由操作队列分配给它的,如果一个操作队列将线程分配给操作对象,那么你需要知道这个线程它是属于操作队列的,你不应该和你的操作对象有任何瓜葛,尤其是,你不应该有任何数据相关的在这个线程中,而这些数据并不是你创建和管理的数据,因为线程的生存和死亡是有操作队列或者系统来控制的,因此在线程间数据传递通过线程来传递将是不可靠,也是容易失败的。 在操作队列上,没有任何的理由可以使用线程存储数据,当你初始化一个操作对象,你应该提供给它所有执行任务需要的数据去做这件事,因此,操作对象要提供所有数据,所有来的、去的数据都应该存在操作对象上,知道任务结束或者应用不在需要它。

3.7.1.2 如果需要的话保持操作对象的引用

因为操作对象是异步运行的,你不应该认为你可以创建或者忘记他们,它们也只是对象而已,也需要你去管理他们,尤其是你需要在它完成之后获取数据的情况下。 你需要保持对操作对象的引用,主要还有可能你再没有机会获取这个操作对象的引用的机会了,操作对象是执行很快的,在很多情况下,操作任务一旦添加到操作队列中,就会被执行,当你的代码获取到从操作队列中拿到的操作对象的时候,很可能,这个操作对象已经结束了,并从操作队列移除了,释放了。

3.7.2 处理错误和异常

由于操作对象在你的应用里面是完全独立的实例,因此有必要去处理那些异常和错误,在OSX 10.6及以后,默认的start的函数不在提供捕获异常。你自己的代码需要去直接的捕获和处理异常,如果需要也要检测错误代码并通知给应用程序,如果你替换了start函数,那么你的代码需要在程序离开底层线程之前就去处理这些异常。 你可能处理的错误可能有一下几种情况 检测UNIX errno 类型的错误 检测显式的由代码返回的错误 捕获来自你自己的代码和系统框架带来的异常 当一个操作对象没有准备好,start方法就开始执行的时候 当一个操作已经在执行或者执行完成了,或取消了,start函数被再次调用的。 当添加一个block任务,但是它已经被执行或者完成的。 当你准备从NSInvocationOperation对象获取数据,但是它已经被取消的情况。 如果你自定义的代码遇到异常或者错误,你需要提供操作步骤给你的应用去处理,Operation对象不会显式的把错误代码或者异常传递给你应用的其他地方,因此,如果对于应用程序是比较重要的信息,你需要提供相关必要的代码去处理这些错误。

3.8 决定操作对象合适的数量,不要太多,也不要太少

虽然可以给一个操作队列中,添加大量的操作对象,但是这么做,往往并不太好,像其他对象一样,创建NSOperation对象也是需要消耗内存,并且他们的执行也是需要开销的。如果你的每个任务都是很小的,而你又创建了成千上万个,你会发现你会消耗更多的时间在分发操作对象上,而不是在执行真正的任务,而如果你的设备已经内存不足时,你会发现成千上万的任务会严重影响程序的性能。 高效的应用操作对象的关键在于,在充分利用设备和执行一定数量的操作任务之间找到平衡点,尝试去找到你的设备执行任务的一个真正的数量值。举个例子,如果你的应用创建了100个操作对象去处理100个不同的值,那么你可以考虑使用10个操作对象,去处理10个值去代替。 同时,你也需要避免一次性的给操作队列中添加大量的任务,也要避免不停的给操作队列以比它处理速度更快的添加更多的任务,相比于一次泛哄式的添加任务,不如批量式的去添加,然后利用完成block回调去执行下一批次的任务。这样可以避免内存过多的消耗。 当然,创建操作对象的数量、以及没个操作对象需要执行的任务量,是由你的程序动态去管理的。你应该是用性能测试工具,例如Instruments去找到性能处理和速度的平衡点,关于Instruments 和其他性能工具的更多信息,参见性能。

3.9 操作对象的执行

最后,你的应用需要执行这些操作对象以真正的处理任务,在这一小节,你将学到就像你熟练的执行你的操作一样,去执行这些操作。

3.9.1 添加操作对象到操作队列中

到目前为止,最容易的执行操作的方式是使用操作队列,即NSOperationQueue类的实例,你的应用有责任去创建和维护这些操作队列。一个应用程序可以有任意数量的操作队列,但是实际上在同一时间有多少个操作会被执行还是有限制的,被系统调用的操作队列会根据可利用的CPU和系统的载入量来进行限制单位时间执行的操作对象,因此,创建更多的操作队列并不意味着可以执行更多的操作对象 就想创建其他的对象一样,在你的应用中创建操作队列

1
NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];

添加一个操作对象到操作队列中,你可以使用addOperation方法,在OSX 10.6以后,你还可以添加操作对象组,通过

1
addOperations:waitUntilFinished:

方法,你也可以直接添加blocl对像到操作队列中(而不需要与一个操作对象关联),通过

1
addOperationWithBlock:

方法,上面的这些方法,队列都会入一个操作对象并通知队列去执行他们,在大多数情况下,操作对象将会被很快执行,但是有时候,操作队列也可能延迟执行操作对象,大概有这么几个原因,最普遍的就是,操作对象间可能有依赖关系,还有可能是操作队列自己可能被挂起,或者是操作队列执行操作任务的数量达到了上限。下面的代码展示了最基础的添加操作到操作队列的语法

1
2
3
4
[aQueue addOperation:anOp]; // Add a single operation 
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations
[aQueue addOperationWithBlock:^{ /* Do something. */
}];

重要 千万不要在一个操作对象进入操作队列之后再去修改它,因为当操作在操作队列中等待执行的时候,它可能在任何时候去执行任务,改变它的依赖和数据,可能会起到坏的作用。如果你想知道一个操作对象的状态,请使用操作对象的属性 去获取。

3.9.2 手动执行操作对象

尽管操作队列对于运行操作对象已经很方便了,但是还是有一种可能不使用操作队列去执行一个操作对象,如果你选择手动去执行操作对象,那么有一些注意事项,最主要的是,操作对象必须得是ready,而且你需要用start函数去启动它。 一个操作对象如果不是ready状态的话不应该去执行,isReady函数被Operation对象的依赖关系封装到了上层,只有当它的依赖关系都清除的时候,一个操作才能够被执行、 当执行一个操作对象,应该去使用start函数去做。用这个函数而不是用main,是因为start函数会在执行之前执行一个安全检查,尤其是,默认的start函数还会产生KVO通知,以保证依赖关系能够正确进行,这个函数同时也避免当你的操作对象被取消的时候,再去执行,以及当操作对象没有准备好的时候就去调用导致的异常抛出 。 如果你的程序定义了并发的操作对象,你同时需要在启动任务之前考虑isConcurrent。当这个方法返回NO的时候,你就可以考虑是在当前线程中同步的执行还是创建一个新的子线程。然而,这些方法的检测完全取决于你。 下面展示了一个比较简单的,手动开启并发操作对象的一个例子。如果这个函数返回NO,你可以启动一个timer,在之后再去调用它。然后直到它返回YES之后,取消掉Timer。因为这种情况可能在操作对象被取消的时候发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (BOOL)performOperation:(NSOperation*)anOp { 
BOOL ranIt = NO;
if ([anOp isReady] && ![anOp isCancelled]) {
if (![anOp isConcurrent])
[anOp start];
else [NSThread detachNewThreadSelector:@selector(start) toTarget:anOp withObject:nil];
ranIt = YES;
}else if ([anOp isCancelled]){
// If it was canceled before it was started, // move the operation to the finished state.
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
// Set ranIt to YES to prevent the operation from
// being passed to this method again in the future.
ranIt = YES;
} return ranIt;
}

3.9.3 取消操作对象

当将操作对象添加到操作队列里面的时候,一个操作对象的管理就交给了操作队列,也不能被移除了,唯一使操作对象出列的方法就是调用它的cancel函数,你也可以取消所有的操作对象,通过调用操作队列的cancelAllOperations 你可以在确保你不在需要操作对象的时候取消掉它。发出一个cancel命令,会将操作对象的关键路径变为canceled状态,这将阻止任务继续执行,由于canceled的操作对象也认为是完成的,那么依赖它的操作对象就可以移除依赖关系。然而,更多情况下,更常用的是取消所有操作独享,在某些重要的时候,比如应用退出、或者用户发出了取消指令,这比一个个取消要好很多。

3.9.4 等待操作对象的完成

为了获取最佳的性能,你应该尽可能的让操作对象并发执行,让系统去干更多的事情,在你执行操作对象任务的时候,如果创建一个操作对象的时候,同时希望获得了操作对象的结果,你可以使用waitUntilFinished:方法来阻塞代码继续执行,直到这个操作对象执行完成。通常来讲,这是最好的方式去避免你能帮到它的时候,阻塞当前线程也许是一个比较好的解决方法。但这也带来了更多的同步性,而限制了整体的可并发性。 你绝不允许在主线程中调用这个方法,你最好在子线程或者其他的线程操作中这么做,阻塞主线程将会降低程序的可响应性。 除了等待一个任务完成,你可以等待一个操作队列所有的任务执行完成,通过调用waitUntilAllOperationsAreFinished,当你等待一个操作队列任务完成的时候,避免在别的线程中添加任务给它,以防延长等待时间。

3.9.5 挂起和恢复操作队列

如果你想中途终止一个操作队列的话,你可以挂起相应的操作队列,使用setSuspended方法,挂起一个操作队列并不会导致正在执行的操作任务终止。它只是会阻止别的操作对象(操作队列中的)不去执行,你可能会在用户想暂停任务的时候挂起所有操作,因为用户还期望在某个时候恢复这些任务。