读 Concurrency Programming Guide 笔记(一)

本文首发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时,需要将任务封装为一个函数或者一个blockblock是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对象创建好后,可以调用它父类NSOperationstart方法执行任务,但是这种不放在操作队列中的执行方式都是在当前线程,也就是主线程中同步执行的。

创建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方法调用的私有方法,或者属性的getset方法。下面以一个网络请求的例子展示如何创建自定义的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中可以看到只有两个方法initmain,前者是该类的初始化方法,主要作用是初始化url这个参数,后者包含了任务的主体逻辑代码,我们来分析一下代码:

  1. 我们在初始化MyNonconcurrentOperation时,传入了我们希望请求的网络地址,改地址正确与否关系着我们这个任务是否还值得继续往下走,所以在main方法一开始先判断一下url的合法性,示例代码中判断的很简单,实际中应该使用正则表达式去判断一下。
  2. 将字符串URL转换为NSURL
  3. 创建NSURLSession实例。
  4. 调用NSURLSession实例的dataTaskWithURL方法,创建NSURLSessionDataTask类的实例,用于请求网络。在completionHandler的闭包中去判断请求是否成功,返回数据是否正确以及解析数据等操作。
  5. 执行NSURLSessionDataTask请求网络。

当我们调用MyNonconcurrentOperationstart方法时,就会执行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值的判断,没错,这就是响应终止执行任务的关键,因为当调用了NSOperationcancel方法后,cancelled属性就会被置为flase,当判断到该属性的值为false时,代表当前任务已经被取消,我们只需释放资源返回即可。我们只有在整个任务逻辑代码中尽可以细的去判断cancelled属性,才可以达到较为实时的终止效果。上面代码中我分别在四个地方判断了cancelled属性:

  1. 在任务开始之前。
  2. 任务开始不久,这里刚创建了NSURLNSURLSession,所以如果判断出任务已被取消,则要释放它们的内存地址。
  3. 开始请求网络之前,这里同样要释放已经创建的变量内存地址。
  4. 网络请求期间。

自定义并发Operation对象

自定义并发Operation对象其主要实现的就是让任务在当前线程以外的线程执行,相对于非并发Operation对象注意的事项要更多一些,我们先来看要实现的两个方法:

  • init:该方法和非并发Operation对象中的作用一样,用于初始化一些属性。
  • start:该方法是自定义并发Operation对象必须要重写父类的一个方法,通常就在这个方法里创建二级线程,让任务运行在当前线程以外的线程中,从而达到并发异步执行任务的目的,所以这个方法中绝对不能调用父类的start方法。
  • main:该方法在非并发Operation对象中就说过,这里的作用的也是一样的,只不过在并发Operation对象中,该方法并不是必须要实现的方法,因为在start方法中就可以完成所有的事情,包括创建线程,配置执行环境以及任务逻辑,但我还是建议将任务相关的逻辑代码都写在该方法中,让start方法只负责执行环境的设置。

除了上述这三个方法以外,还有三个属性需要我们重写,就是NSOperation类中的executingfinishedconcurrent三个属性,这三个属性分别表示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")

}

}

由于NSOperationfinishedexecutingconcurrent这三个属性都是只读的,我们无法重写它们的setter方法,所以我们只能靠新建的私有属性去重写它们的getter方法。为了自定义的Operation对象更像原生的NSOperation子类,我们需要通过willChangeValueForKeydidChangeValueForKey方法手动为ifFinishedifExecuting这两个属性生成KVO通知,将keyPath设置为原生的finishedexecuting

上面的代码示例中有几个关键点:

  • start方法开始之初就要判断一下Operation对象是否被终止任务。
  • main方法中的内容要放在autoreleasepool中,解决在二级线程中的内存释放问题。
  • 如果判断出Operation对象的任务已经被终止,要及时修改ifFinishedifExecuting属性。

我们可以测试一下这个自定义的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()
分享到: