本文首发CSDN,如需转载请与CSDN联系。
并发任务是指多个任务在某一时刻同时运行。在过去,一提到并发执行任务,首当其冲的解决方案就是在程序中创建多个线程来实现,但是线程本身较为底层,而且管理的难度比较大,如果想做倒最优的线程数量、最恰当的线程创建销毁时机是很难的,以至于虽然达到了并发执行任务的目的,但却以降低程序性能为代价,所以往往得不偿失。
鉴于上述的原因,于是一些实现并发任务的其他方案出现了。在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()
原文出处:读 Concurrency Programming Guide 笔记(二)
本文首发CSDN,如需转载请与CSDN联系。
Operation对象的相关设置
Operation对象除了上文中讲到到基本使用方法外还有一些其他的特性,这些特性需要根据我们的应用场景去设置,设置的时机在创建Operation对象之后和运行它或者将其放入操作队列之前,下面让我们来看看Operation对象还有哪些特性。
Operation对象之间的依赖
与GCD不同,Operation Queue不遵循先进先出的原则,而且Operation Queue始终是并发执行Operation对象的,所以想让Operation对象串行执行就需要用它的Operation对象依赖特性,该特性可以让Operation对象将自己与另外一个Operation对象进行关联,并且当关联的Operation对象执行完成后才可以执行,这样就达到了串行执行Operation对象的目的。
我们可以用NSOperation的addDependency方法添加依赖的Operation对象,而且产生依赖的这两个Operation对象并不要求必须在相同的操作队列中,但是这种依赖只能是单向的,不能相互依赖。
import Foundation
class TestOperationDependency {
func launch() {
let blockOperationA = NSBlockOperation(block: {
print("Task in blockOperationA...")
sleep(3)
})
let blockOperationB = NSBlockOperation(block: {
print("Task in blockOperationB...")
sleep(5)
})
blockOperationA.addDependency(blockOperationB)
let operationQueue = NSOperationQueue()
operationQueue.addOperation(blockOperationA)
operationQueue.addOperation(blockOperationB)
sleep(10)
}
}
let testOperationDependency = TestOperationDependency()
testOperationDependency.launch()
上面的示例代码展示了如何给Operation对象添加依赖,大家可以注释掉blockOperationA.addDependency(blockOperationB)这一行看看打印结果有什么区别。
Operation对象的优先级
上文中说了,操作队列里的Operation对象都是并发执行的,如果一个操作队列中有多个Operation对象,那么谁先执行谁后执行取决于Operation对象的依赖Operation对象是否已执行完成,也就是是否处于准备执行的状态。其实Operation对象自身也有优先级的属性,如果有两个都处于准备执行状态的Operation对象,那么优先级高的会先执行,优先级低的后执行。每个Operation对象默认的优先级是NSOperationQueuePriority.Normal级别,我们可以通过设置queuePriority属性更改Operation的在队列中执行的优先级,优先级别有以下五种:
NSOperationQueuePriority.Normal:正常优先级NSOperationQueuePriority.Low:低优先级NSOperationQueuePriority.VeryLow:非常低优先级NSOperationQueuePriority.High:高优先级NSOperationQueuePriority.VeryHigh:非常高优先级
这里我们需要注意一下Operation对象优先级的作用域,它只能作用于相同的操作队列中,不同操作队列中的Operation对象是不受优先级影响的。另外需要注意的是,如果有两个Operation对象,一个处于准备执行状态,但优先级比较低,另一个处于等待状态,但优先级比较高,那么此时仍然是处于准备执行状态的低优先级Operation对象先执行。可见Operation对象的优先级相互影响需要满足两个条件,一是必须处在同一个操作队列中,另一个是Operation对象都处于准备执行状态。
通过Operation对象修改线程优先级
通常情况下,线程的优先级由内核自己管理,不过在OS X v10.6及以后的版本和iOS4到iOS7期间,NSOperation多了一个threadPriority属性,我们可以通过该属性设置Operation对象运行所在线程的优先级,数值范围为0.0到1.0,数字越高优先级越高。不过可能是出于线程安全等方面的考虑,Apple从iOS8开始废除了该属性。
设置Completion Block
上篇文章中说过,Operation对象其中的一个特别好的特性就是完成时回调闭包Completion Block。它的作用不言而喻,就是当主要任务执行完成之后做一些收尾的处理工作,我们可以设置completionBlock属性给Operation对象添加完成时回调闭包:
blockOperationA.completionBlock = {
print("blockOperationA has finished...")
}
执行Operation对象
虽然前面文章的示例中已经包含了对Operation对象的执行,但是并没详细说明,这节就说说Operation对象的执行。
使用Operation Queue
使用Operation Queue操作队列执行Operation对象已然是标配选项了,操作队列在Cocoa框架中对应的类是NSOperationQueue,一个操作队列中可以添加多个Operation对象,但一次到底添加多少Operation对象得根据实际情况而定,比如应用程序对内存的消耗情况、内核的空闲情况等,所以说凡事得有度,不然反而会适得其反。另外需要注意的一点是不论有多少个操作队列,它们都受制于系统的负载、内核空闲等运行情况,所以说并不是说再创建一个操作队列就能执行更多的Operation对象。
在使用操作队列时,我们首先要创建NSOperationQueue的实例:
let operationQueue = NSOperationQueue()
然后通过NSOperationQueue的addOperation方法添加Operation对象:
operationQueue.addOperation(blockOperationA)
operationQueue.addOperation(blockOperationB)
在OS X v10.6之后和iOS4之后,我们还可以用addOperations:waitUntilFinished:方法添加一组Operation对象:
operationQueue.addOperations([blockOperationA, blockOperationB], waitUntilFinished: false)
该方法有两个参数:
ops: [NSOperation]:Operation对象数组。waitUntilFinished wait: Bool:该参数标示这个操作队列在执行Operation对象时是否会阻塞当前线程。
我们还可以通过addOperationWithBlock方法向操作队列中直接添加闭包,而不需要去创建Operation对象:
operationQueue.addOperationWithBlock({
print("The block is running in Operation Queue...")
})
除了以上这几种添加Operation对象的方法外,还可以通过NSOperationQueue的maxConcurrentOperationCount属性设置同时执行Operation对象的最大数:
operationQueue.maxConcurrentOperationCount = 2
如果设置为1,那么不管该操作队列中添加了多少Operation对象,每次都只运行一个,而且会按照添加Operation对象的顺序去执行。所以如果遇到添加到操作的队列的Operation对象延迟执行了,那么通常会有两个原因:
- 添加的Operation对象数超过了操作队列设置的同时执行Operation对象的最大数。
- 延迟执行的Operation对象在等待它依赖的Operation对象执行完成。
另外需要的注意的是当Operation对象添加到操作队列中后,不要再更改它任务中涉及到的任何属性或者它的依赖,因为到操作队列中的Operation对象随时会被执行,所以如果你自以为它还没有被执行而去修改它,可能并不会达到你想要的结果。
手动执行Operation对象
除了用操作队列来执行Operation对象以外,我们还可以手动执行某个Operation对象,但是这需要我们注意更多的细节问题,也要写更多的代码去确保Operation对象能正确执行。在上篇文章中,我们创建过自定义的Operation对象,其中我们知道有几个属性特别需要我们注意,那就是ready、concurrent、executing、finished、cancelled,对应Operation对象是否出于准备执行状态、是否为异步并发执行的、是否正在执行、是否已经执行完成、是否已被终止。这些状态在我们使用操作队列时都不需要理会,都有操作队列帮我们把控判断,确保Operation对象的正确执行,我们只需要在必要的时候获取状态信息查看而已。但是如果手动执行Operation对象,那么这些状态都需要我们来把控,因为你手动执行一个Operation对象时要判断它的依赖对象是否执行完成,是否被终止了等等,所以并不是简单的调用start方法,下面来看看如果正确的手动执行Operation对象:
func performOperation(operation: NSOperation) -> Bool {
var result = false
if operation.ready && !operation.cancelled {
if operation.concurrent {
operation.start()
} else {
NSThread.detachNewThreadSelector("start", toTarget: operation, withObject: nil)
}
result = true
}
return result
}
终止Operation对象执行
一旦Operation对象被添加到操作队列中,这个Operation对象就属于这个操作队列了,并且不能被移除,唯一能让Operation对象失效的方法就是通过NSOperation的cancel方法终止它执行,或者也可以通过NSOperationQueue的cancelAllOperations方法终止在队列中的所有Operation对象。
暂停和恢复操作队列
在实际运用中,如果我们希望暂停操作队列执行Operation对象,可以通过设置NSOperationQueue的suspended属性为false来实现,不过这里要注意的是暂停操作队列只是暂停执行下一个Operation对象,而不是暂停当前正在执行的Operation对象,将suspended属性设置为true后,操作队列则恢复执行。
Dispatch Queues
Dispatch Queue是GCD中的核心功能,它能让我们很方便的异步或同步执行任何被封装为闭包的任务,它的运作模式与Operation Queue很相似,但是有一点不同的是Dispatch Queue是一种先进先出的数据结构,也就是执行任务的顺序永远等同于添加任务时的顺序。GCD中已经为我们提供了几种类型的Dispatch Queue,当然我们也可以根据需求自己创建Dispatch Queue,下面我们先来看看Dispatch Queue的类型:
- 串行Dispatch Queue:该类型的队列一次只能执行一个任务,当前任务完成之后才能执行下一个任务,而且可依任务的不同而在不同的线程中执行,这类队列通常作为私有队列使用。这里需要注意的是虽然该类型的队列一次只能执行一个任务,但是可以让多个串行队列同时开始执行任务,达到并发执行的任务的目的。
- 并行Dispatch Queue:该类队列可同时执行多个任务,但是执行任务的顺序依然是遵循先进先出的原则,同样可依任务的不同而在不同的线程中执行,这类队列通常作为全局队列使用。
- 主Dispatch Queue:该类队列实质上也是一个串行队列,但是该队列是一个全局队列,在该队列中执行的任务都是在当前应用的主线程中执行的。通常情况下我们不需要自己创建此类队列。
Dispatch Queue与Operation Queue相似,都能让我们更方便的实现并发任务的编程工作,并且能提供更优的性能,因为我们不再需要编写关于线程管理相关的一大堆代码,这些完全都有系统接管,我们只需要将注意力放在要执行的任务即可。举个简单的例子,如果有两个任务需要在不同的线程中执行,但是他们之间存在资源竞争的情况,所以需要保证执行的先后顺序,如果我们自己创建线程实现该场景,那么就务必要用的线程锁机制,确保任务有正确的执行顺序,这势必对系统资源的开销会非常大,如果使用Dispatch Queue,我们只需要将任务安正确的顺序添加到串行队列中即可,省时省力省资源。
任务的载体是闭包
在使用Dispatch Queue时,需要将任务封装为闭包。闭包就是一个函数,或者一个指向函数的指针,加上这个函数执行的非局部变量,闭包最大的一个特性就是可以访问父作用域中的局部变量。我们在将任务封装为闭包进行使用时要注意以下这几点:
- 虽然在闭包中可以使用父作用域中的变量,但是尽可能少的使用父作用域中比较大的变量以及不要在闭包中做类似删除清空父作用域中变量的行为。
- 当将一个封装好任务的闭包添加至Dispatch Qeueu中,Dispatch Queue会自动复制该闭包,并且在执行完成后释放该闭包,所以不同担心闭包中一些值的变化问题,以及资源释放问题。
- 虽然使用Dispatch Queue执行并发异步任务很方便,但是创建和执行闭包还是有一定资源开销的,所以尽量不要使用Dispatch Queue执行一些很小的任务,要物有所值。如果确实有很小的任务需要并发异步执行,那么使用
NSThread的detachNewThreadSelector方法或NSObject的performSelectorInBackground方法去执行也未必不可。 - 如果同一个队列中的多个任务之间需要共享数据,那么应该使用队列上下文去存储数据,供不同的任务访问。
- 如果闭包中的任务创建了不少对象,那么应该考虑将整个任务逻辑代码放在
autoreleasepool中,虽然Dispatch Queue中也有自动释放池,但是你不能保证它每次释放的时间,所以咱们自己再加一个要来的更保险一些。
创建与管理Dispatch Queues
在使用Dispatch Queue之前,我们首先需要考虑应该创建什么类型的Dispatch Queue,如何进行配置等,这一节就来说一说如何创建和管理Dispatch Queue。
全局并发Dispatch Queue
并发队列的好处人人皆知,可以方便的同时处理多个任务,在GCD中并发Dispatch Queue同样遵循先进先出的原则,但这只是在运行时适用,如果有个任务在并发队列中还没轮到它执行,那么此时完全可以移除它,而不必等它前面的任务执行完成之后。至于并发队列中没次有多少个任务在执行,这个恐怖在每一秒都在变化,因为影响它的因素有很多,所以之前说过,尽量不要移除移除已经添加进队列的任务。
OS X和iOS系统为我们提供了四种全局并发Dispatch Queue,所谓全局队列,就是我们不需要理会它们的保留和释放问题,而且不需要专门创建它。与其说是四种不如说是一种全局并发队列的四种不同优先级,因为它们之间唯一的不同之处就是队列优先级不同。与Operation Queue不同,在GCD中,Dispatch Queue只有四种优先级:
DISPATCH_QUEUE_PRIORITY_HIGH:高优先级。DISPATCH_QUEUE_PRIORITY_DEFAULT:默认优先级,低于高优先级。DISPATCH_QUEUE_PRIORITY_LOW:低优先级,低于高优先级和默认优先级。DISPATCH_QUEUE_PRIORITY_BACKGROUND:后台优先级,低于高优先级和后台线程执行的任务。
我们可以通过dispatch_get_global_queue函数再根据不同的优先级获取不同的全局并发队列,类型为dispatch_queue_t:
let highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
let defaultPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
我们在使用全局并发队列的时候不需要保留队列的引用,随时要用随时用该函数获取即可。当然我们也可以通过dispatch_queue_create函数自己创建队列:
let concurrentQueue = dispatch_queue_create("com.example.MyConcurrentQueue", DISPATCH_QUEUE_CONCURRENT)
从上面代码可以看到,dispatch_queue_create函数有两个参数,第一个为队列的名称,第二个为队列类型,串行队列为DISPATCH_QUEUE_SERIAL,并发队列为DISPATCH_QUEUE_CONCURRENT。
串行Dispatch Queue
串行队列可以让我们将任务按照一定顺序执行,能更优的处理多个任务之间的资源竞争问题,比线程锁机制有更小的资源开销和更好的性能,并且不会产生死锁的问题。
系统也为我们提供了一个串行队列,我们可以通过dispatch_get_main_queue函数获取:
let mainQueue = dispatch_get_main_queue()
该队列与当前应用的主线程相关联。当然我们也可以自己创建串行队列:
let serialQueueA = dispatch_queue_create("com.example.MySerialQueueA", DISPATCH_QUEUE_SERIAL)
// 或者
let serialQueueB = dispatch_queue_create("com.example.MySerialQueueB", nil)
dispatch_queue_create函数的第二个参数如果为nil则默认创建串行队列。当我们创建好串行队列后,系统会自动将创建好的队列与当前应用的主线程进行关联。
获取当前队列
如果需要验证或者测试当前队列,我们可以通过dispatch_get_current_queue函数获取当前队列。如果在闭包中调用,返回的是该闭包所在的队列,如果在闭包外调用,返回的则是默认的并发队列。不过该函数在OS X v10.10中和Swift中都不能使用了,取而代之的是通过DISPATCH_CURRENT_QUEUE_LABEL属性的get方法。
擅用队列上下文
很多情况下,同一个队列中的不同任务之间需要共享数据,尤其像串行队列中的任务,可能由多个任务对某个变量进行处理,或者都需要使用到某个对象,这时就要用到队列上下文:
import Foundation
class TestDispatchQueue {
func launch() {
let serialQueue = dispatch_queue_create("com.example.MySerialQueue", DISPATCH_QUEUE_SERIAL)
dispatch_set_context(serialQueue, unsafeBitCast(0, UnsafeMutablePointer<Int>.self))
dispatch_async(serialQueue, {
var taskCount = unsafeBitCast(dispatch_get_context(serialQueue), Int.self)
taskCount++
print("TaskA in the dispatch queue...and The number of task in queue is \(taskCount)")
dispatch_set_context(serialQueue, unsafeBitCast(taskCount, UnsafeMutablePointer<Int>.self))
sleep(1)
})
dispatch_async(serialQueue, {
var taskCount = unsafeBitCast(dispatch_get_context(serialQueue), Int.self)
taskCount++
print("TaskB in the dispatch queue...and The number of task in queue is \(taskCount)")
dispatch_set_context(serialQueue, unsafeBitCast(taskCount, UnsafeMutablePointer<Int>.self))
})
sleep(3)
}
}
let testDispatchQueue = TestDispatchQueue()
testDispatchQueue.launch()
从上面的代码示例中可以看到,在执行代码点,我们用dispatch_set_context函数向serialQueue队列的上下文环境中设置了一个Int类型的变量,初始值为0。该函数有两个参数,第一个是目标队列,第二个参数是上下文数据的指针。然后在闭包中我们使用dispatch_get_context函数获取上下文数据进行进一步的处理。除了基本类型,我们也可以将自定义的类放入队列上下文中:
import Foundation
class Contact: NSObject {
let name = "DevTalking"
let mobile = "10010"
}
class TestDispatchQueue {
let contact = Contact()
func launch() {
let serialQueue = dispatch_queue_create("com.example.MySerialQueue", DISPATCH_QUEUE_SERIAL)
dispatch_set_context(serialQueue, unsafeBitCast(contact, UnsafeMutablePointer<Void>.self))
dispatch_async(serialQueue, {
let contact = unsafeBitCast(dispatch_get_context(serialQueue), Contact.self)
print("The name is \(contact.name)")
sleep(1)
})
dispatch_async(serialQueue, {
let contact = unsafeBitCast(dispatch_get_context(serialQueue), Contact.self)
print("The name is \(contact.mobile)")
})
sleep(3)
}
}
let testDispatchQueue = TestDispatchQueue()
testDispatchQueue.launch()
关于
unsafeBitCast函数和Swift中指针的用法在这里可以有所参考。
队列的收尾工作
虽然在ARC时代,资源释放的工作已经基本不需要我们手动去做了,但有些时候因为系统释放资源并不是很及时,也会造成内存移除等问题,所以在一些情况下我们还是需要进行手动释放资源的工作,必入添加autoreleasepool保证资源及时释放等。Dispatch Queue也给我们提供了这样的机会(机会针对于ARC时代,在MRC时代是必须要做的),那就是Clean Up Function清理扫尾函数,当队列被释放时,或者说引用计数为0时会调用该函数,并且将上下文指针也传到了该函数,以便进行清理工作:
import Foundation
class Contact: NSObject {
let name = "DevTalking"
let mobile = "10010"
}
class TestDispatchQueue {
let contact = Contact()
func testCleanUpFunction() {
launch()
sleep(15)
}
func launch() {
let serialQueue = dispatch_queue_create("com.example.MySerialQueue", DISPATCH_QUEUE_SERIAL)
dispatch_set_context(serialQueue, unsafeBitCast(contact, UnsafeMutablePointer<Void>.self))
dispatch_set_finalizer_f(serialQueue, myFinalizerFunction())
dispatch_async(serialQueue, {
let contact = unsafeBitCast(dispatch_get_context(serialQueue), Contact.self)
print("The name is \(contact.name)")
sleep(1)
})
dispatch_async(serialQueue, {
let contact = unsafeBitCast(dispatch_get_context(serialQueue), Contact.self)
print("The name is \(contact.mobile)")
})
sleep(3)
}
func myFinalizerFunction() -> dispatch_function_t {
return { context in
let contact = unsafeBitCast(context, Contact.self)
print("The name is \(contact.name) and the mobile is \(contact.mobile), The serialQueue has been released and we need clean up context data.")
// TODO...
}
}
}
let testDispatchQueue = TestDispatchQueue()
testDispatchQueue.testCleanUpFunction()
从上面的代码示例中可以看到当给队列设置完上下文时,我们使用了dispatch_set_finalizer_f函数给队列设置清理函数,dispatch_set_finalizer_f函数有两个参数,第一个是目标队列,第二个参数是类型为dispatch_function_t的函数指针,也就是清理函数,上下文数据指针是该函数唯一的参数。在上面代码中,我们添加了myFinalizerFunction函数作为清理函数,在该函数中获得上下文数据,然后进行后续的清理工作。