iOS并发开发简要整理

在多核处理器没有大范围使用开来的时候,就有了多线程的概念,iOS的并发开发也不是新东西了。本篇文章主要是对iOS开发当中经常涉及到的并发内容所做的简要整理,把学过的用过的东西重新整理到笔头上。

0. 并发的初衷

记得大学上专业课的时候,老师曾经经常提问一个问题——“并发”vs.“并行”。简单对比下,并行基本上就是同时进行,而并发不一定保证两个线程同一时刻在同时作业,而可以体现为同一个时间段内各自都完成了一部分任务。

在多核上,做的好的并发可以真正的并行,而在单核上,不过是多个线程对处理器计算执行权的交换更替。为什么要交替进行?这好比一群人排队等着填表做体检,轮到一个人A的时候,进行了一半发现需要等另一人B到达带来一些材料,B迟迟不来,那么在他身后排队的人C(以及C后面的人)肯定不会就这样停下,而是更希望A到一边等B,C和后面的人依次完成任务,知道B来了,A继续。这样的好处就是,体检可以更有效率的进行,总体上缩短这群人完成体检的时间,C和C后面的人不用陪着A一起死等B的到来。计算机也是一样,处理器资源宝贵,如果有现成等待IO完成,其它准备好了的线程可以先来执行。并发既能提高吞吐量,也能让准备好的线程有机会得到比较好的响应。

在之前我整理Java并发文章的时候也提到过这些。

1. iOS平台的并发支持

我这里强调“iOS并发”,是因为我主要是在iOS开发工作中了解到的这些技术,而这些技术并不一定仅局限于iOS系统。从一定意义上讲,iOS是源于苹果的桌面系统OS X的,所以这里提到的很多东西也是在整个苹果开发体系内可用的。

经过了苹果技术体系的不断发展,iOS上对并发的支持从不同层面来看可谓是多种多样。目前苹果官方文档主推的技术是Grand Central Dispatch(GCD)和NSOperation /Operation Queue,这两个都是使用任务队列的方式进行并发任务的执行实现。除了这些,Cocoa(Touch)的Foundation里还有一个NSThread类,这个类相信很多人就比较容易理解的,也很类似java.lang.Thread了,NSObject本身也可以分出NSThread来。在iOS开发当中,C语言的东东也都是可用的,所以像pthread也可以发挥它的作用。

iOS中除了上面提到的这些并发的技术支持外,还有一个概念经常和并发一起出现,Run Loop。后面的文章中,我会整理一下,把我的理解阐述一下。

2. GCD

GCD是苹果提供的一个简单、直接的能够实现并发的方案,需要和block配合使用。在iOS4之后,block的广泛使用,也让GCD成为了一个很好用的工具。之前只用“线程”概念做并发开发的同学,可能不是很容易理解GCD(至少我当初有点迷惑),但如果你做过Java,想想ThreadPoolExecutor就应该明白个大概了。

比较原始的,我们可以创建一个线程,指定它的任务,令其执行。多个任务就开多个线程。但线程不是越多越好,线程会占用资源,而且线程间的切换也会影响执行效率。所以,我们就要考虑如何维护线程。再结合线程安全,线程同步等问题考虑,线程管理的成本还是很高的。而如果线程的维护管理是系统已经为我们实现好的,按照一定的策略进行管理调度,那可以大大减轻我们的维护工作,降低成本。所以。无论是iOS上的GCD,还是Java的ThreadPoolExecutor都是这样做的,我们最后只管往一个队列里放入我们想要执行的任务即可。

  • GCD中,一个队列就是一个 dispatch_queue_t类型的东东,它在GCD中也算是一个Dispatch Object了。队列从类型上来看分串行的(Serial)和并发的(Concurrent)的,区别在于前者必须等一个任务完成后,下一个任务(block)才开始进行,后者则不保证。不过两者都是按顺序向队列里放任务进去的。此外,苹果文档中还单独给出了一个主队列(main queue),主队列其实也是一个串行队列,与主线程关联,由main runloop控制,用来处理UI相关的任务。
  • GCD队列具体怎么创建和获取,可参考苹果文档,我这里不会细说。但是需要说明一点的是,主队列不用自己创建,另外还有全局的(Global)几个不同优先级的队列也可以直接获取到,除非自己需要特殊的串行队列,这些大多数情况下够用了。现在串行的队列和并发的队列都可以通过 dispatch_queue_create来创建,根据参数区分。但自己创建的要自己维护,负责清理释放。
  • 向队列中分派任务,可以以同步的方式进行( dispatch_sync),也可以是异步的( dispatch_async)。同步的方式会在分派任务时阻塞住,直到结束。但考虑可能发生的死锁情况,同步还是尽量不用的好。除非在能保证安全的情况下,又需要直接拿到同步执行的返回值的。
  • dispatch_once是个很不错的东西,可以用来实现单例,也主要用来实现单例。Xcode5.1.1中貌似就可以自动完成用dispatch_once实现的代码块,很赞很方便。
  • 至于dispatch_apply、dispatch_after这种我就不多说了,很容易懂,也没有其它几个常用。dispatch_group则实现了Thread概念下类似Join的功能,等待一个或一组任务完成。
  • GCD任务分派后没法取消,但可以通过GCD队列进行suspend和resume控制。而针对并发队列(Concurrent Queue),可以通过barrier对特定任务进行前后隔离,读写锁实现可考虑。需要注意的是,本段中这几点,对于Global Queue是无效的!
  • GCD队列可组合使用。在GCD队列中的任务出队列时不一定就执行了,也可能进到另一个队列。而Global队列有优先级之分,因此可以通过队列组合指定优先级,方法就是dispatch_set_target_queue()。
GCD队列图

GCD队列图(来源于Objc中国)

  • 对于阻塞的调用,尽管脱离开主线程,使得App的UI不受影响,但特定线程的资源还是会得不到充分利用,可考虑使用dispatch source作异步解决方案。

GCD简单易用,是并发开发很好上手的工具,这里就把主要的内容整理至此。

除了上面提到的这些,GCD还有很多其它内容,还有信号量(semaphore)、dispatch data、dispatch io等。除了参考苹果的《Concurrency Programming Guide》所述,《GCD Reference》东西更全。

3. Operation Queue

说完了GCD,Operation Queue理解起来就简单多了。可以说,Operation Queue是在语言上Objective-C的一个包装,实际上也是这样,最终的实现可以认为还是利用了GCD。

既然是包装了GCD,那一定多做了一些事情,最基本的就是面向对象,是Objective-C中NSObject的子类对象,可继承扩展,可封装复用。除此之外,Operation支持KVO、支持优先级、支持completion block,但我最关注的还是另外两点:

  • 支持cancel
  • 支持任务间依赖(注意,依赖高于优先级priority)

有了这两点支持,在某些复杂的业务场景下,用Operation比用GCD省事很多。

按照苹果官方文档所述,NSOperation是“抽象”的(虽然Objective-C中没有真正意义上的抽象概念),需要继承、扩展实现,方可使用。当然,苹果还是给了两个可用的子类,NSBlockOperation和NSInvocationOperation。这些怎么用苹果文档里写得都很清楚,注意NSBlockOperatioan里的block是并发的。

对于自己扩展实现的Operation,最主要的最基本的,把main方法实现了。做好异常处理是好习惯。

关于Operation的执行,既可以放到Operation Queue里(通常是这么用),也可以单独start。放到NSOperationQueue对象里会增加operation的引用计数,同时也保证了异步执行。而如果是调用start进行执行就不一定了,能否异步全看这个Operation是如何实现的。Operation的实现类别分为同步和并发的,可通过isConcurrent区别。默认情况下,这个方法返回NO,意味着调用start方法单独执行时都是在当前线程执行的。如果需要并发,就要负责自己对Operation的并发特性进行实现:

  • 首先,不能再用父类的start方法实现,需要自己负责开启新线程执行自定义的任务。
  • isConcurrent方法自然要修改为返回YES。
  • 同时,对isFinished和isExecuting等方法需要按新的自定义逻辑进行实现,同时保持对KVO的兼容。

关于Operation的取消执行支持,这个是之前提到的Operation的一个特点。而这也是需要开发者参与定义的,在执行构成中的特定点判断是否需要终止执行。需要注意的一点是,被cancel掉的Operation同样也被认为是finished状态的,这意味着一个依赖于其它Operation的Operation,只要它的依赖全部都cancel了,也可以开始start执行了。此外,也可以对一个Operation Queue对象直接做取消操作,取消队列中所有任务。

4. 其它

不管是GCD还是Operation Queue,都没有对传统线程(Thread)的直接使用,对线程模型进行了包装,避免了一些比较繁琐的问题,降低了对线程对象的维护成本。那么想用好这两种工具,我们需要做的就是对要处理的任务进行合理抽象拆分,交给合适的队列处理。和线程要占用系统资源一样,NSOperation对象也要恰当使用,不是越多越好,要控制在合适的范围。

就像前文提并发的初衷所说,并发是为了更合理更有效率的利用计算资源,而较慢的IO处理是一个阻力。并发和IO通常是紧密关联的两个话题。在实际开发中,即使使用GCD和NSOperation对任务做了并发异步处理,也尽量不要让任务出现阻塞,因为GCD和NSOperation底层也是依赖线程实现的,一个阻塞的动作也会使一个线程对应的资源变得浪费。对于这种情况,我们尽量不要占用线程,尽量利用系统所提供的各项异步IO方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章