导读:本文为读《Concurrency Programming Guide》笔记第一篇,并发执行任务的解决方案从最初的在程序中创建多个线程来实现,却极大地降低了应用程序的性能,由此进化出多种实现并发执行任务的解决方案,比如GCD、Operation Queues等。在文章中,作者付宇轩(@DevTalking)对在OS X和iOS应用开发中实现任务异步执行的技术以及应注意的事项进行了分享。
并发任务是指多个任务在某一时刻同时运行。在过去,一提到并发执行任务,首当其冲的解决方案就是在程序中创建多个线程来实现,但是线程本身较为底层,而且管理的难度比较大,如果想做倒最优的线程数量、最恰当的线程创建销毁时机是很难的,以至于虽然达到了并发执行任务的目的,但却以降低程序性能为代价,所以往往得不偿失。
鉴于上述的原因,于是一些实现并发任务的其他方案出现了。在OS X和iOS系统中采用了多种实现并发执行任务的方法,与直接创建线程不同,这些方法让开发者只需要关注要执行的任务,然后让系统执行它们即可,不需要关心线程管理的问题,为开发者提供了一个简单而高效的并发任务编程模式。
其中一种实现任务异步执行的技术就是Grand Central Dispatch(GCD),该技术封提供了系统级别的线程管理功能,我们在使用它时只需要定义我们希望执行的任务,然后将任务添加到对应的分派执行队列中即可。另外一个技术是Operation queues,具体的实现是Objective-C中的 NSOperationQueue
对象,它的作用和GCD很相似,同样只需要我们定义好任务,然后添加到对应的操作队列中即可,其他与线程管理相关的事都由 NSOperationQueue
帮我们完成。
Dispatch Queues是基于C语言的,执行自定义任务的技术,从字面意思理解其实就是执行任务的队列,使用GCD执行的任务都是放在这个队列中执行的,当然队列的数量可以有多个,类型也不止一种。一个Dispatch queue可以串行的执行任务,也可以并行的执行任务,但不管哪种执行任务的方式,都遵循先进先出的原则。串行队列一次只能执行一个任务,当前任务执行完后才能执行下一个任务,并且执行任务的顺序和添加任务的顺序是一致的。并行队列自然是可同时执行多个任务,不需要等待上个任务完成后才执行下个任务。我们来看看Dispatch queue还有哪些好的特性:
使用Dispatch Queue时,需要将任务封装为一个函数或者一个 block
, block
是Objective-C中对闭包的实现,在OS X 10.6和iOS 4.0时引入的,在Swift中直接为闭包。
Dispatch Source是GCD中的一个基本类型,从字面意思可称为调度源,它的作用是当有一些特定的较底层的系统事件发生时,调度源会捕捉到这些事件,然后可以做其他的逻辑处理,调度源有多种类型,分别监听对应类型的系统事件。我们来看看它都有哪些类型:
Dispatch Source是GCD中很有意思也很有用的一个特性,根据不同类型的调度源,我们可以监听较为底层的系统行为,不论在实现功能方面还是调试功能方面都非常游有用,后文中会再详细讲述。
Operation Queue与Dispatch Queue很类似,都是有任务队列或操作队列的概念,只不过它是由Cocoa框架中的 NSOperationQueue
类实现的,它俩最主要的区别是任务的执行顺序,在Dispatch Queue中,任务永远都是遵循先进先出的原则,而Operation Queue加入了其他的任务执行顺序特性,使下一个任务的开始不再取决于上个任务是否已完成。
上文说过,使用Dispatch Queue时,需要将任务封装为一个函数或者闭包。而在Operation Queue中,需要将任务封装为一个 NSOpertaion
对象,然后放入操作队列执行。同时该对象还自带键值观察(KVO)通知特性,可以很方便的监听任务的执行进程。
虽然并发执行任务可以提高程序对用户操作的响应速度,最大化使用内核,提升应用的效率,但是这些都是建立在正确合理使用并发任务技术,以及应用程序确实需要使用这类技术的前提下。如果使用不得当,或者对简单的应用程序画蛇添足,那么反而会因为使用了并发任务技术而导致应用程序性能下降,另一方面开发人员面对的代码复杂度也会增加,维护成本同样会上升。所以在准备使用这类技术前一定要三思而行,从性能、开发成本、维护成本等多个方面去考虑是否需要使用并发任务技术。
考虑是否需要用只是第一步,当确定使用后更不能盲目的就开始开发,因为并发任务技术的使用需要侵入应用程序的整个开发生命周期,所以在应用开发之初,就是考虑如何根据这类技术去设计并发任务,考虑应用中任务的类型、任务中使用的数据结构等等,否则亡羊补牢也为时已晚。这一节主要说说在设计并发任务时应该注意哪些事。
在动手写代码前,尽量根据需求,穷举应用中的任务以及在任务中涉及到的对象何数据结构,然后分析这些任务的优先级和触发类型,比如罗列出哪些任务是由用户操作触发的,哪些是任务是无需用户参与触发的。
当把任务根据优先级梳理好后,就可以从高优先级的任务开始逐个分析,考虑任务在执行过程中涉及到哪些对象和数据结构,是否会修改变量,被修改的变量是否会对其他变量产生影响,以及任务的执行结果对整个程序产生什么影响等。举个简单的例子,如果一个任务中对某个变量进行了修改,并且这个变量不会对其他变量产生影响,而且任务的执行结果也相对比较独立,那么像这种任务就最合适让它异步去执行。
任务可以是一个方法,也可以是一个方法中的一段逻辑,不论是一个方法还是一段逻辑,我们都可以从中拆分出若干个执行单元,然后进一步分析这些执行单元,如果多个执行单元必须得按照特定得顺序执行,而且这一组执行单元的执行结果想对独立,那么可以将这若干执行单元视为执行单元组,可以考虑让该执行单元组异步执行,其他不需要按照特定顺序的执行单元可以分别让它们异步执行。可以使用的技术可以用GCD或者Operation Queue。
在拆分执行单元时,尽量拆的细一点,不要担心执行单元的数量过多,因为GCD和Operation Queue有着高性能的线程管理机制,不需要担心过多的使用任务队列会造成性能损耗。
当我们将任务分解为一个个执行单元并分析之后,下一步就是将这些执行单元封装在 block
中或者封装为 NSOperation
对象来使用GCD或Operation Queues,但在这之前还需要我们根据执行单元确定好适合的队列,不管是Dispatch queue还是Operation queue,都需要明确是使用串行队列还是并行队列,确定是将多个执行单元放入一个队列中还是分别放入多个队列中,以及使用正确优先级的队列。
在使用任务队列时注意以下几点,可以有效的提高执行效率:
UIView
提供的一系列动画的方法等。 Operation Queue技术由Cocoa框架提供,用于实现任务并发异步执行的技术,该技术基于面向对象概念。该技术中最主要的两个元素就是Operation对象和Operation队列,我们先来看看Operation对象。
Operation对象的具体实现是Foundation框架中的 NSOperation
类,它的主要作用就是将我们希望执行的任务封装起来,然后去执行。 NSOperation
类本身是一个抽象类,在使用时需要我们创建子类去继承它,实现一些父类的方法,以达到我们使用的需求。同时Foundation框架也提供了两个已经实现好的 NSOperation
子类,供我们方便的使用:
NSInvocationOperation
:当我们已经有一个方法需要异步去执行,此时显然没有必要为了这一个方法再去创建一个 NSOperation
的子类,所以我们就可以用 NSInvocationOperation
类来封装这个方法,然后放入操作队列去执行,以满足我们的需求。 NSBlockOperation
:该类可以让我们同时执行多个 block
对象或闭包。 同时所有继承 NSOperation
的子类都会具有如下特性:
虽然Operation Queues技术主要是通过将Operation对象放入队列中,实现并发异步的执行任务,但是我们也可以直接通过 NSOperation
类的 start
方法让其执行任务,但这样就属于同步执行任务了,我们还可以通过 NSOperation
类的 isConcurrent
方法来确定当前任务正在异步执行还是同步执行。
上文中已经提到过, NSInvocationOperation
对象是Foundation框架提供的 NSOperation
抽象类的实现,主要作用是方便我们将已有对象和方法封装为Operation对象,然后放入操作队列执行目标方法,同时该对象的好处是可以避免我们为已有的对象的方法逐个创建Operation对象,避免冗余代码。不过,由于 NSInvocationOperation
不是类型安全的,所以从Xcode 6.1开始,在Swift中就不能再使用该对象了。我们可以看看在Objective-C中如何创建该对象:
@implementation MyCustomClass - (NSOperation*)taskWithData:(id)data { NSInvocationOperation* theOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(myTaskMethod:) object:data]; return theOp; } - (void)myTaskMethod:(id)data { // Perform the task. } @end
当 NSInvocationOperation
对象创建好后,可以调用它父类 NSOperation
的 start
方法执行任务,但是这种不放在操作队列中的执行方式都是在当前线程,也就是主线程中同步执行的。
NSBlockOperation
是另外一个由Foundation框架提供的 NSOperation
抽象类的实现类,该类的作用是将一个或多个block或闭包封装为一个Operation对象。在第一次创建 NSBlockOperation
时至少要添加一个block:
import Foundation class TestBlockOperation { func createBlockOperationObject() -> NSOperation { print("The main thread num is /(NSThread.currentThread())") let nsBlockOperation = NSBlockOperation(block: { print("Task in first closure. The thread num is /(NSThread.currentThread())") }) return nsBlockOperation } } let testBlockOperation = TestBlockOperation() let nsBlockOperation = testBlockOperation.createBlockOperationObject() nsBlockOperation.start()
上面的代码中我们首先打印了主线程的线程号,然后通过 createBlockOperationObject
方法创建了一个 NSBlockOperation
对象,在初始化时的block中同样打印了当前线程的线程号,调用它父类的方法 start
后,可以看到这个block中的任务是在主线程中执行的:
The main thread id is <NSThread: 0x101502e40>{number = 1, name = main} Task in first closure. The thread id is <NSThread: 0x101502e40>{number = 1, name = main}
然而我们也可以通过 NSBlockOperation
对象的方法 addExecutionBlock
添加其他的block或者说任务:
import Foundation class TestBlockOperation { func createBlockOperationObject() -> NSOperation { print("The main thread num is /(NSThread.currentThread())") let nsBlockOperation = NSBlockOperation(block: { print("Task in first closure. The thread num is /(NSThread.currentThread())") }) // 第一种写法 nsBlockOperation.addExecutionBlock({ print("Task in second closure. The thread num is /(NSThread.currentThread())") }) // 第二种写法 nsBlockOperation.addExecutionBlock{ print("Task in third closure. The thread num is /(NSThread.currentThread())") } return nsBlockOperation } } let testBlockOperation = TestBlockOperation() let nsBlockOperation = testBlockOperation.createBlockOperationObject() nsBlockOperation.start()
当我们再执行 NSBlockOperation
时,可以看到后面添加的两个任务都在不同的二级线程中执行,此时这个任务为并发异步执行:
The main thread id is <NSThread: 0x101502e40>{number = 1, name = main} Task in first closure. The thread id is <NSThread: 0x101502e40>{number = 1, name = main} Task in third closure. The thread id is <NSThread: 0x101009190>{number = 2, name = (null)} Task in second closure. The thread id is <NSThread: 0x101505110>{number = 3, name = (null)}
通过上面两段代码可以观察到,当 NSBlockOperation
中只有一个block时,在调用 start
方法执行任务时不会为其另开线程,而是在当前线程中同步执行,只有当 NSBlockOperation
包含多个block时,才会为其另开二级线程,使任务并发异步执行。另外,当 NSBlockOperation
执行时,它会等待所有的block都执行完成后才会返回执行完成的状态,所以我们可以用 NSBloxkOperation
跟踪一组block的执行情况。
如果 NSInvocationOperation
对象和 NSBlockOperation
对象都不能满足我们的需求,那么我们可以自己写一个类去继承 NSOperation
,然后实现我们的需求。在实现自定义Operation对象时,分并发执行任务的Operation对象和非并发执行任务的Operation对象。
实现非并发Operation对象相对要简单一些,通常,我们最少要实现两个方法:
main
方法:该方法就是处理主要任务的地方,你需要执行的任务都在这个方法里。 当然除了上面两个必须的方法外,也可以有被 main
方法调用的私有方法,或者属性的 get
、 set
方法。下面以一个网络请求的例子展示如何创建自定义的Operation对象:
import Foundation class MyNonconcurrentOperation: NSOperation { var url: String? init(withURL url: String) { self.url = url } override func main() { // 1. guard let strURL = self.url else { return } // 2. var nsurl = NSURL(string: strURL) // 3. var session: NSURLSession? = NSURLSession.sharedSession() // 4. var dataTask: NSURLSessionDataTask? = session!.dataTaskWithURL(nsurl!, completionHandler: { (nsdata, nsurlrespond, nserror) in if let error = nserror { print("出现异常:/(error.localizedDescription)") } else { do { let dict = try NSJSONSerialization.JSONObjectWithData(nsdata!, options: NSJSONReadingOptions.MutableContainers) print(dict) } catch { print("出现异常") } } }) // 5. dataTask!.resume() sleep(10) } } let myNonconcurrentOperation = MyNonconcurrentOperation(withURL: "http://www.baidu.com/s?wd=ios") myNonconcurrentOperation.start()
我们创建了自定义的Operation类 MyNonconcurrentOperation
,让其继承 NSOperation
,在 MyNonconcurrentOperation
中可以看到只有两个方法 init
和 main
,前者是该类的初始化方法,主要作用是初始化 url
这个参数,后者包含了任务的主体逻辑代码,我们来分析一下代码:
MyNonconcurrentOperation
时,传入了我们希望请求的网络地址,改地址正确与否关系着我们这个任务是否还值得继续往下走,所以在 main
方法一开始先判断一下 url
的合法性,示例代码中判断的很简单,实际中应该使用正则表达式去判断一下。 NSURL
。 NSURLSession
实例。 NSURLSession
实例的 dataTaskWithURL
方法,创建 NSURLSessionDataTask
类的实例,用于请求网络。在 completionHandler
的闭包中去判断请求是否成功,返回数据是否正确以及解析数据等操作。 NSURLSessionDataTask
请求网络。 当我们调用 MyNonconcurrentOperation
的 start
方法时,就会执行 main
方法里的逻辑了,这就是一个简单的非并发自定义Operation对象,之所以说它是非并发,因为它一般都在当前线程中执行任务,既如果你在主线程中初始化它,调用它的 start
方法,那么它就在主线程中执行,如果在二级线程中进行这些操作,那么就在二级线程中执行。
注:如果在二级线程中使用非并发自定义Operation对象,那么 main
方法中的内容应该使用 autoreleasepool{}
包起来。因为如果在二级线程中,没有主线程的自动释放池,一些资源没法被回收,所以需要加一个自动释放池,如果在主线程中就不需要了。
一般情况下,当Operation对象开始执行时,就会一直执行任务,不会中断执行,但是有时需要在任务执行一半时终止任务,这时就需要Operation对象有响应任务终止命令的能力。理论上,在Operation对象执行任务的任何时间点都可以调用 NSOperation
类的 cancel
方法终止任务,那么在我们自定义的Operation对象中如何实现响应任务终止呢?我们看看下面的代码:
import Foundation class MyNonconcurrentOperation: NSOperation { var url: String? init(withURL url: String) { self.url = url } override func main() { // 1. if self.cancelled { return } guard let strURL = self.url else { return } var nsurl = NSURL(string: strURL) var session: NSURLSession? = NSURLSession.sharedSession() // 2. if self.cancelled { nsurl = nil session = nil return } var dataTask: NSURLSessionDataTask? = session!.dataTaskWithURL(nsurl!, completionHandler: { (nsdata, nsurlrespond, nserror) in if let error = nserror { print("出现异常:/(error.localizedDescription)") } else { // 4. if self.cancelled { nsurl = nil session = nil return } do { let dict = try NSJSONSerialization.JSONObjectWithData(nsdata!, options: NSJSONReadingOptions.MutableContainers) print(dict) } catch { print("出现异常") } } }) // 3. if self.cancelled { nsurl = nil session = nil dataTask = nil return } dataTask!.resume() sleep(10) } } let myNonconcurrentOperation = MyNonconcurrentOperation(withURL: "http://www.baidu.com/s?wd=ios") myNonconcurrentOperation.start() myNonconcurrentOperation.cancel()
从上述代码中可以看到,在 main
方法里加了很多对 self.cancelled
值的判断,没错,这就是响应终止执行任务的关键,因为当调用了 NSOperation
的 cancel
方法后, cancelled
属性就会被置为 flase
,当判断到该属性的值为 false
时,代表当前任务已经被取消,我们只需释放资源返回即可。我们只有在整个任务逻辑代码中尽可以细的去判断 cancelled
属性,才可以达到较为实时的终止效果。上面代码中我分别在四个地方判断了 cancelled
属性:
NSURL
和 NSURLSession
,所以如果判断出任务已被取消,则要释放它们的内存地址。 自定义并发Operation对象其主要实现的就是让任务在当前线程以外的线程执行,相对于非并发Operation对象注意的事项要更多一些,我们先来看要实现的两个方法:
init
:该方法和非并发Operation对象中的作用一样,用于初始化一些属性。 start
:该方法是自定义并发Operation对象必须要重写父类的一个方法,通常就在这个方法里创建二级线程,让任务运行在当前线程以外的线程中,从而达到并发异步执行任务的目的,所以这个方法中绝对不能调用父类的 start
方法。 main
:该方法在非并发Operation对象中就说过,这里的作用的也是一样的,只不过在并发Operation对象中,该方法并不是必须要实现的方法,因为在 start
方法中就可以完成所有的事情,包括创建线程,配置执行环境以及任务逻辑,但我还是建议将任务相关的逻辑代码都写在该方法中,让 start
方法只负责执行环境的设置。 除了上述这三个方法以外,还有三个属性需要我们重写,就是 NSOperation
类中的 executing
、 finished
、 concurrent
三个属性,这三个属性分别表示Operation对象是否在执行,是否执行完成以及是否是并发状态。因为并发异步执行的Operation对象并不会阻塞主线程,所以使用它的对象需要知道它的执行情况和状态,所以这三个状态是必须要设置的,下面来看看示例代码:
import Foundation class MyConcurrentOperation: NSOperation { var url: String? private var ifFinished: Bool private var ifExecuting: Bool override var concurrent: Bool { get { return true } } override var finished: Bool { get { return self.ifFinished } } override var executing: Bool { get { return self.ifExecuting } } init(withURL url: String) { self.url = url self.ifFinished = false self.ifExecuting = false } override func start() { if self.cancelled { self.willChangeValueForKey("finished") self.ifFinished = true self.didChangeValueForKey("finished") return } else { self.willChangeValueForKey("executing") NSThread.detachNewThreadSelector("main", toTarget: self, withObject: nil) self.ifExecuting = true self.didChangeValueForKey("executing") } } override func main() { autoreleasepool{ guard let strURL = self.url else { return } var nsurl = NSURL(string: strURL) var session: NSURLSession? = NSURLSession.sharedSession() if self.cancelled { nsurl = nil session = nil self.completeOperation() return } var dataTask: NSURLSessionDataTask? = session!.dataTaskWithURL(nsurl!, completionHandler: { (nsdata, nsurlrespond, nserror) in if let error = nserror { print("出现异常:/(error.localizedDescription)") } else { if self.cancelled { nsurl = nil session = nil self.completeOperation() return } do { let dict = try NSJSONSerialization.JSONObjectWithData(nsdata!, options: NSJSONReadingOptions.MutableContainers) print(dict) self.completeOperation() } catch { print("出现异常") self.completeOperation() } } }) if self.cancelled { nsurl = nil session = nil dataTask = nil self.completeOperation() return } dataTask!.resume() } } func completeOperation() { self.willChangeValueForKey("finished") self.willChangeValueForKey("executing") self.ifFinished = true self.ifExecuting = false self.didChangeValueForKey("finished") self.didChangeValueForKey("executing") } }
由于 NSOperation
的 finished
、 executing
、 concurrent
这三个属性都是只读的,我们无法重写它们的 setter
方法,所以我们只能靠新建的私有属性去重写它们的 getter
方法。为了自定义的Operation对象更像原生的 NSOperation
子类,我们需要通过 willChangeValueForKey
和 didChangeValueForKey
方法手动为 ifFinished
和 ifExecuting
这两个属性生成KVO通知,将 keyPath
设置为原生的 finished
和 executing
。
上面的代码示例中有几个关键点:
start
方法开始之初就要判断一下Operation对象是否被终止任务。 main
方法中的内容要放在 autoreleasepool
中,解决在二级线程中的内存释放问题。 ifFinished
和 ifExecuting
属性。 我们可以测试一下这个自定义的Operation对象:
import Foundation class Test: NSObject { private var myContext = 0 let myConcurrentOperation = MyConcurrentOperation(withURL: "http://www.baidu.com/s?wd=ios") func launch() { myConcurrentOperation.addObserver(self, forKeyPath: "finished", options: .New, context: &myContext) myConcurrentOperation.addObserver(self, forKeyPath: "executing", options: .New, context: &myContext) myConcurrentOperation.start() sleep(5) print(myConcurrentOperation.executing) print(myConcurrentOperation.finished) print(myConcurrentOperation.concurrent) sleep(10) } override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) { if let change = change where context == &myContext { if keyPath == "finished" { print("Finish status has been changed, The new value is /(change[NSKeyValueChangeNewKey]!)") } else if keyPath == "executing" { print("Executing status has been changed, The new value is /(change[NSKeyValueChangeNewKey]!)") } } } deinit { myConcurrentOperation.removeObserver(self, forKeyPath: "finished", context: &myContext) myConcurrentOperation.removeObserver(self, forKeyPath: "executing", context: &myContext) } } let test = Test() test.launch()
作者简介: 付宇轩(@DevTalking),从事Java中间件开发、iOS开发。主要主持开发企业级ETL中间件、BPM中间件、企业级移动应用,个人博客地址: http://www.devtalking.com 。