并发任务是指多个任务在某一时刻同时运行。在过去,一提到并发执行任务,首当其冲的解决方案就是在程序中创建多个线程来实现,但是线程本身较为底层,而且管理的难度比较大,如果想做倒最优的线程数量、最恰当的线程创建销毁时机是很难的,以至于虽然达到了并发执行任务的目的,但却以降低程序性能为代价,所以往往得不偿失。
鉴于上述的原因,于是一些实现并发任务的其他方案出现了。在OS X和iOS系统中采用了多种实现并发执行任务的方法,与直接创建线程不同,这些方法让开发者只需要关注要执行的任务,然后让系统执行它们即可,不需要关心线程管理的问题,为开发者提供了一个简单而高效的并发任务编程模式。
其中一种实现任务异步执行的技术就是Grand Central Dispatch(GCD),该技术封提供了系统级别的线程管理功能,我们在使用它时只需要定义我们希望执行的任务,然后将任务添加到对应的分派执行队列中即可。另外一个技术是Operation queues,具体的实现是Objective-C中的NSOperationQueue对象,它的作用和GCD很相似,同样只需要我们定义好任务,然后添加到对应的操作队列中即可,其他与线程管理相关的事都由NSOperationQueue帮我们完成。
Dispatch Queues简述
Dispatch Queues是基于C语言的,执行自定义任务的技术,从字面意思理解其实就是执行任务的队列,使用GCD执行的任务都是放在这个队列中执行的,当然队列的数量可以有多个,类型也不止一种。一个Dispatch queue可以串行的执行任务,也可以并行的执行任务,但不管哪种执行任务的方式,都遵循先进先出的原则。串行队列一次只能执行一个任务,当前任务执行完后才能执行下一个任务,并且执行任务的顺序和添加任务的顺序是一致的。并行队列自然是可同时执行多个任务,不需要等待上个任务完成后才执行下个任务。我们来看看Dispatch queue还有哪些好的特性:
- 有简单宜用,通俗易懂的编程接口。
- 提供了自动管理的线程池。
- 可自动调节队列装载任务的速度。
- 更优的内存使用率。
- 使用户不用担心死锁的问题。
- 提供了比线程锁更优的同步机制。
使用Dispatch Queue时,需要将任务封装为一个函数或者一个block,block是Objective-C中对闭包的实现,在OS X 10.6和iOS 4.0时引入的,在Swift中直接为闭包。
Dispatch Sources简述
Dispatch Source是GCD中的一个基本类型,从字面意思可称为调度源,它的作用是当有一些特定的较底层的系统事件发生时,调度源会捕捉到这些事件,然后可以做其他的逻辑处理,调度源有多种类型,分别监听对应类型的系统事件。我们来看看它都有哪些类型:
- Timer Dispatch Source:定时调度源。
- Signal Dispatch Source:监听UNIX信号调度源,比如监听代表挂起指令的SIGSTOP信号。
- Descriptor Dispatch Source:监听文件相关操作和Socket相关操作的调度源。
- Process Dispatch Source:监听进程相关状态的调度源。
- Mach port Dispatch Source:监听Mach相关事件的调度源。
- Custom Dispatch Source:监听自定义事件的调度源。
Dispatch Source是GCD中很有意思也很有用的一个特性,根据不同类型的调度源,我们可以监听较为底层的系统行为,不论在实现功能方面还是调试功能方面都非常游有用,后文中会再详细讲述。
Operation Queues简述
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,都需要明确是使用串行队列还是并行队列,确定是将多个执行单元放入一个队列中还是分别放入多个队列中,以及使用正确优先级的队列。
提高效率的其他技巧
在使用任务队列时注意以下几点,可以有效的提高执行效率:
- 如果应用比较吃内存,那么建议在任务中直接计算一些需要的值,这样比从主存中加载要来的快。
- 尽早确定顺序执行的任务,尽量将其改为并行任务,比如说有多个任务存在资源竞争问题,那么可以根据情况分别为每个任务拷贝一份该资源,从而避免顺序执行任务,以提高执行效率。
- 避免使用线程锁机制。在使用GCD或Operation Queues技术时基本不需要使用线程锁,因为有串行队列的存在。
- 尽量使用系统提供的框架达到并发任务的目的,一些系统提供的框架本身就有一些方法函数可以让任务并发执行,比如UIView提供的一系列动画的方法等。
Operation Queues
Operation Queue技术由Cocoa框架提供,用于实现任务并发异步执行的技术,该技术基于面向对象概念。该技术中最主要的两个元素就是Operation对象和Operation队列,我们先来看看Operation对象。
Operation Objects
Operation对象的具体实现是Foundation框架中的NSOperation类,它的主要作用就是将我们希望执行的任务封装起来,然后去执行。NSOperation类本身是一个抽象类,在使用时需要我们创建子类去继承它,实现一些父类的方法,以达到我们使用的需求。同时Foundation框架也提供了两个已经实现好的NSOperation子类,供我们方便的使用:
- NSInvocationOperation:当我们已经有一个方法需要异步去执行,此时显然没有必要为了这一个方法再去创建一个NSOperation的子类,所以我们就可以用NSInvocationOperation类来封装这个方法,然后放入操作队列去执行,以满足我们的需求。
- NSBlockOperation:该类可以让我们同时执行多个block对象或闭包。
同时所有继承NSOperation的子类都会具有如下特性:
- 可自动管理Operation对象之间的依赖关系,举个例子,当一个Operation对象执行之前发现它包含的任务中有依赖其他的Operation对象,并且该Operation对象还没有执行完成,那么当前的Operation对象会等待它的依赖执行完成后才会执行。
- 支持可选的完成时回调闭包,该闭包可以在Operation对象包含的主要任务执行完之后执行。
- 自带键值观察(KVO)通知特性,可以监听任务的执行状态。
- 可在运行时终止任务执行。
虽然Operation Queues技术主要是通过将Operation对象放入队列中,实现并发异步的执行任务,但是我们也可以直接通过NSOperation类的start方法让其执行任务,但这样就属于同步执行任务了,我们还可以通过NSOperation类的isConcurrent方法来确定当前任务正在异步执行还是同步执行。
创建NSInvocationOperation对象
上文中已经提到过,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对象
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的执行情况。
自定义Operation对象
如果NSInvocationOperation对象和NSBlockOperation对象都不能满足我们的需求,那么我们可以自己写一个类去继承NSOperation,然后实现我们的需求。在实现自定义Operation对象时,分并发执行任务的Operation对象和非并发执行任务的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的合法性,示例代码中判断的很简单,实际中应该使用正则表达式去判断一下。
- 将字符串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对象其主要实现的就是让任务在当前线程以外的线程执行,相对于非并发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中,解决在二级线程中的内存释放问题。
- 如果判断出Operation对象的任务已经被终止,要及时修改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() |
|