什么是ShadowSocks

ShadowSocks是基于Socks5协议,使用类似SSH隧道的方式收发网络请求的一款开源软件,该软件由 @clowwindy 开发,最初只有Python版本,随后出现C++、C#、Go等多语言版本。ShadowSocks最大的特点是可以保护网络流量、加密数据传输,可有效防止GFW(Great Firewall of China)封杀网络请求。

用ShadowSocks科学上网的概念

在以前,我们访问互联网的资源都是简单而直接的,用户的请求发送到资源服务方,比如Google、Facebook等,然后资源服务方直接将内容响应给用户,世界多么美好。

但是,在1998年时候,中国创建了互联网边界审查系统,称之为中国国家防火墙(GFW),这堵墙横在了用户和互联网资源服务方之间,用于监控和过滤互联网国际出口上的内容,监控国际网关的通讯,对认为不匹配国家官方要求的传输内容,进行干扰、阻断、屏蔽。

从此之后好多有价值的网站就被堵在了墙后。

但是由于对知识的渴望,人们想到了绕过GFW的办法,那就是在境外搭建一个国内用户的代理,国内用户与代理之间建立加密的通道,由境外代理请求被墙的网络资源,再通过加密通道返回给国内用户。代理的类型也有多种,像HTTP、Socks、VPN、SSH等。以SSH隧道为例:

因为SSH本身基于RSA加密技术,所以GFW就无法对数据传输过程加密的数据进行分析,从而避免被重置链接、阻断、屏蔽等问题。

但是GFW也不会懵B一世,人家也会学习,由于在创建SSH隧道的过程中有较为明显的特性,所以GFW还是可以通过分析连接的特性进行干扰。此时ShadowSocks横空出世,先看看图示:

简单来说,ShadowSocks分为客户端和服务端,用户发出的请求基于Socks5协议与ShadowSocks客户端进行通信,一般情况下SS客户端都在本机,通过ShadowSocksX、GoAgentX等应用启动,所以这一步是不会经过GFW的,然后ShadowSocks提供了多种加密方式供客户端和服务端之间进行通信,并且在经过GFW时是普通的TCP协议数据包,没有明显的特征,而且GFW也无法解密分析,从而实现绕墙访问资源。

閱讀全文 »

本文首发CSDN,如需转载请与CSDN联系。

通过Dispatch Queue执行任务

如果想让Dispatch Queue执行任务,首先就是得将任务放入队列中,我们可以异步的将任务加入队列,也可以同步的将任务加入队列,可以一个任务一个任务的加,也可以一组一组的加。这节我们就来看看将任务加入队列的那些事。

向队列添加任务

我们可以使用dispatch_async或者dispatch_async_f函数异步的向队列中添加任务,也就是说当我们添加完任务后该函数会立即返回,我们不需要等待任务执行完成,而且我们也不会知道队列到底何时开始执行任务。dispatch_async函数有两个参数,一个是目标队列,类型为dispatch_queue_t,另一个是闭包,类型为dispatch_block_t

let serialQueue = dispatch_queue_create("com.example.MySerialQueue", nil)

dispatch_async(serialQueue, {

print("Task in the queue...")

})

dispatch_async_f函数有三个参数,第一个是类型为dispatch_queue_t的目标队列,第二个是队列上下文指针,第三个是类型为dispatch_function_t的任务函数,队列上下文指针为该函数的唯一参数:

class AddTaskToQueue {

func launch() {

let serialQueue = dispatch_queue_create("com.example.MySerialQueue", nil)

dispatch_async(serialQueue, {

print("Task in the queue...")

})

dispatch_async_f(serialQueue, unsafeBitCast(0, UnsafeMutablePointer<Int>.self), taskFunction())

sleep(3)

}

func taskFunction() -> dispatch_function_t {

return { context in

print("Do some work with context...")

}

}

}

let addTaskToQueue = AddTaskToQueue()
addTaskToQueue.launch()

除了这两个函数,我们还可以使用dispatch_syncdispatch_sync_f函数同步的向队列中添加任务,并且我们要等待直到任务执行完成。这两个函数和上面的异步添加任务函数用法完全一致。

那么什么时候用异步什么时候用同步呢,大多数情况下我们都是在主线程中使用GCD分派任务,为了避免阻塞主线程,影响用户体验,所以通常情况下我们都使用异步添加任务的方式。当然为了避免任务与主线程中产生资源竞争的问题,有时候酌情也会使用同步添加任务的方式。

Dispatch Queue的Completion Block

还记得NSOperationcompletionBlock属性吗,这个回调函数在任务执行完成后调用,用于处理有些后续工作或者消息通知。在Dispatch Queue中并没有类似的属性,但是我们可以通过其他方式来实现。举一个很常见的应用场景,我们在主线程中分派一个下载图片的任务,让其在二级线程中执行,当图片下载完成后通知主线程,并由主线程将图片显示出来,我们看看简单的代码片段:

class DownloadImage {

func dispatchTaskInMainThread() {

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), downloadImage())

}

func downloadImage() -> (() -> ()) {

return {

print("Downloading image in \(NSThread.currentThread())")

dispatch_async(dispatch_get_main_queue()) {

print("Handle image and display in \(NSThread.currentThread())")

}

}

}

}

我们来看看上面代码都做了些什么,首先在dispatchTaskInMainThread方法中,我们使用dispatch_get_global_queue函数获取到全局并发队列,然后将downloadImage下载图片的方法作为任务添加到该全局队列中。在downloadImage方法里,当图片下载完成后通过dispatch_get_main_queue函数获取到主队列,也就是在主线程中对图片进行处理,这样我们就达到了Completion Block的效果。

閱讀全文 »

本文首发CSDN,如需转载请与CSDN联系。

Operation对象的相关设置

Operation对象除了上文中讲到到基本使用方法外还有一些其他的特性,这些特性需要根据我们的应用场景去设置,设置的时机在创建Operation对象之后和运行它或者将其放入操作队列之前,下面让我们来看看Operation对象还有哪些特性。

Operation对象之间的依赖

与GCD不同,Operation Queue不遵循先进先出的原则,而且Operation Queue始终是并发执行Operation对象的,所以想让Operation对象串行执行就需要用它的Operation对象依赖特性,该特性可以让Operation对象将自己与另外一个Operation对象进行关联,并且当关联的Operation对象执行完成后才可以执行,这样就达到了串行执行Operation对象的目的。

我们可以用NSOperationaddDependency方法添加依赖的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()

然后通过NSOperationQueueaddOperation方法添加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对象的方法外,还可以通过NSOperationQueuemaxConcurrentOperationCount属性设置同时执行Operation对象的最大数:

operationQueue.maxConcurrentOperationCount = 2

如果设置为1,那么不管该操作队列中添加了多少Operation对象,每次都只运行一个,而且会按照添加Operation对象的顺序去执行。所以如果遇到添加到操作的队列的Operation对象延迟执行了,那么通常会有两个原因:

  • 添加的Operation对象数超过了操作队列设置的同时执行Operation对象的最大数。
  • 延迟执行的Operation对象在等待它依赖的Operation对象执行完成。

另外需要的注意的是当Operation对象添加到操作队列中后,不要再更改它任务中涉及到的任何属性或者它的依赖,因为到操作队列中的Operation对象随时会被执行,所以如果你自以为它还没有被执行而去修改它,可能并不会达到你想要的结果。

手动执行Operation对象

除了用操作队列来执行Operation对象以外,我们还可以手动执行某个Operation对象,但是这需要我们注意更多的细节问题,也要写更多的代码去确保Operation对象能正确执行。在上篇文章中,我们创建过自定义的Operation对象,其中我们知道有几个属性特别需要我们注意,那就是readyconcurrentexecutingfinishedcancelled,对应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对象失效的方法就是通过NSOperationcancel方法终止它执行,或者也可以通过NSOperationQueuecancelAllOperations方法终止在队列中的所有Operation对象。

暂停和恢复操作队列

在实际运用中,如果我们希望暂停操作队列执行Operation对象,可以通过设置NSOperationQueuesuspended属性为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执行一些很小的任务,要物有所值。如果确实有很小的任务需要并发异步执行,那么使用NSThreaddetachNewThreadSelector方法或NSObjectperformSelectorInBackground方法去执行也未必不可。
  • 如果同一个队列中的多个任务之间需要共享数据,那么应该使用队列上下文去存储数据,供不同的任务访问。
  • 如果闭包中的任务创建了不少对象,那么应该考虑将整个任务逻辑代码放在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函数作为清理函数,在该函数中获得上下文数据,然后进行后续的清理工作。

本文首发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()

本文首发CSDN,如需转载请与CSDN联系。

记得第一次读这个文档还是3年前,那时也只是泛读。如今关于iOS多线程的文章层出不穷,但我觉得若想更好的领会各个实践者的文章,应该先仔细读读官方的相关文档,打好基础,定会有更好的效果。文章中有对官方文档的翻译,也有自己的理解,官方文档中代码片段的示例在这篇文章中都进行了完整的重写,还有一些文档中没有的代码示例,并且都使用Swift完成,给大家一些Objc与Swift转换的参考。
官方文档地址:Threading Programming Guide

配置Timer事件源

配置Timer事件源拢共分几步?很简单,大体只有两步,先创建Timer对象,然后将其添加至Run Loop中。在Cocoa框架和Core Foundation框架中都提供了相关的对象和接口,在Cocoa框架中,它为我们提供了NSTimer类,该类有两个类方法,可以让我们很方便的在当前线程的Run Loop中配置Timer事件源:

  • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats::该方法有五个参数分别是执行事件消息时间间隔、接收事件消息的目标对象、事件消息、发送给事件消息的参数、是否重复执行标识。
NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "fireTimer:", userInfo: "This is a arg", repeats: true)

func fireTimer(sender: NSTimer) {

print("Fire timer...\(sender.userInfo as! String)")

}
  • scheduledTimerWithTimeInterval:invocation:repeats::该方法有三个参数,分别是执行事件消息事件间隔、NSInvocation对象、是否重复执行标识。这里说一下NSInvocation类,该类的作用是静态渲染消息,说的简单粗暴一点,那就是该类表示某个对象中的某个方法,以及该方法的一个或多个参数和返回值,当我们需要发送有多个参数或者有返回值的消息时就可以用这个类。但是在Swift中无法使用这个类,这里就不做过多说明了。

以上两个类方法所添加的Timer事件源都只能添加在当前线程的Run Loop中,并且是在默认的Run Loop模式下(NSDefaultRunLoopMode),如果我们想将Timer事件源添加至其他线程Run Loop的其他模式下,那么就需要创建NSTimer对象,并使用NSRunLoopaddTimer:forMode:方法添加创建好的NSTimer对象:

import Foundation

class CustomThread: NSThread {

var myTimer: NSTimer!

init(myTimer: NSTimer) {

self.myTimer = myTimer

}

override func main() {

autoreleasepool{

let runloop = NSRunLoop.currentRunLoop()

runloop.addTimer(self.myTimer, forMode: NSRunLoopCommonModes)

print(NSThread.isMultiThreaded())

runloop.runUntilDate(NSDate(timeIntervalSinceNow: 5))

}

}

}

class TestThread: NSObject {

func testTimerSource() {

let fireTimer = NSDate(timeIntervalSinceNow: 1)

let myTimer = NSTimer(fireDate: fireTimer, interval: 0.5, target: self, selector: "timerTask", userInfo: nil, repeats: true)

let customThread = CustomThread(myTimer: myTimer)

customThread.start()

sleep(5)

}

func timerTask() {

print("Fire timer...")

}

}

let testThread = TestThread()
testThread.testTimerSource()

在Core Foundation框架中,也为我们提供了一系列相关的类和方法为Run Loop添加Timer事件源,我们一起来看看:

import Foundation

class TestThread: NSObject {

func testCFTimerSource() {

let cfRunloop = CFRunLoopGetCurrent()

var cfRunloopTimerContext = CFRunLoopTimerContext(version: 0, info: unsafeBitCast(self, UnsafeMutablePointer<Void>.self), retain: nil, release: nil, copyDescription: nil)

let cfRunloopTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, 1, 0.5, 0, 0, cfRunloopTimerCallback(), &cfRunloopTimerContext)

CFRunLoopAddTimer(cfRunloop, cfRunloopTimer, kCFRunLoopDefaultMode)

CFRunLoopRun()
}

func cfRunloopTimerCallback() -> CFRunLoopTimerCallBack {

return { (cfRunloopTimer, info) -> Void in

print("Fire timer...")

}

}

}

let testThread = TestThread()
testThread.testCFTimerSource()

配置基于端口的事件源

Cocoa框架和Core Foundation框架都提供了创建配置基于端口事件源的类和方法,下面我们来看看如何使用Cocoa框架创建基于端口的事件源以及配置使用该类事件源。

使用NSMachPort对象

NSMachPort对象是什么呢?其实就是线程与线程之间通信的桥梁,我们创建一个NSMachPort对象,将其添加至主线程的Run Loop中,然后我们在二级线程执行的任务中就可以获取并使用该对象向主线程发送消息,也就是说这种方式是将NSMachPort对象在不同线程中相互传递从而进行消息传递的。

在主线程中创建配置NSMachPort

因为NSMachPort只能在OS X系统中使用,所以我们需要创建一个OS X应用的工程我们先来看看代码:

import Cocoa

class ViewController: NSViewController, NSMachPortDelegate {

let printMessageId = 1000

override func viewDidLoad() {

super.viewDidLoad()

let mainThreadPort = NSMachPort()

mainThreadPort.setDelegate(self)

NSRunLoop.currentRunLoop().addPort(mainThreadPort, forMode: NSDefaultRunLoopMode)

let workerClass = WorkerClass()

NSThread.detachNewThreadSelector("launchThreadWithPort:", toTarget: workerClass, withObject: mainThreadPort)

}

// MARK: NSPortDelegate Method

func handlePortMessage(message: NSPortMessage) {

}

}

首先我们看到ViewController类遵循了NSMachPortDelegate协议,因为它要作为NSMachPort的代理类,通过NSMachPortDelegatehandlePortMessage:方法处理来自二级线程的消息。

viewDidLoad方法中我们先是创建了NSMachPort对象的实例,接着设置它的代理,然后使用NSRunLoopaddPort:forMode:方法将创建好的端口对象添加至主线程的Run Loop中,最后通过NSThreaddetachNewThreadSelector:toTarget:withObject:方法创建二级线程,并让该二级线程执行WorkerClass类中的launchThreadWithPort:方法,同时将刚才创建好的端口对象作为参数传给该方法,也就是将主线程中的端口对象传到了二级线程中。下面来看看handlePortMessage:中应该如何处理接收到的消息:

func handlePortMessage(message: NSPortMessage) {

let messageId = message.msgid

if messageId == UInt32(printMessageId) {

print("Receive the message that id is 1000 and this is a print task.")

} else {

// Handle other messages

}

}

通过端口传递的消息可以根据消息编号判断该执行什么样的任务,所以该方法中通过NSPortMessage对象获取到消息id然后进行判断并执行相应的任务,消息id在二级线程通过端口向主线程发送消息时可以设置。

在二级线程中创建配置NSMachPort

首先二级线程中与主线程中一样,都需要创建端口对象、设置代理、将端口对象添加至当前线程的Run Loop中:

import Cocoa

class WorkerClass: NSObject, NSMachPortDelegate {

func launchThreadWithPort(port: NSMachPort) {

autoreleasepool{

let secondaryThreadPort = NSMachPort()

secondaryThreadPort.setDelegate(self)

let runloop = NSRunLoop.currentRunLoop()

runloop.addPort(secondaryThreadPort, forMode: NSDefaultRunLoopMode)

sendPrintMessage(port, receivePort: secondaryThreadPort)

runloop.runMode(NSDefaultRunLoopMode, beforeDate: NSDate(timeIntervalSinceNow: 500))

}

}

func sendPrintMessage(sendPort: NSMachPort, receivePort: NSMachPort) {


}

// MARK: NSPortDelegate Method

func handlePortMessage(message: NSPortMessage) {

}

}

创建并配置好端口后就需要向主线程发送消息了,下面我们来看看sendPrintMessage:receivePort:方法:

func sendPrintMessage(sendPort: NSMachPort, receivePort: NSMachPort) {

let portMessage = NSPortMessage(sendPort: sendPort, receivePort: receivePort, components: nil)

portMessage.msgid = UInt32(1000)

portMessage.sendBeforeDate(NSDate(timeIntervalSinceNow: 1))

}

首先需要创建NSPortMessage对象,该对象就是端口之间相互传递的介质,初始化方法的第一个参数为主线程的端口对象,也就是发送消息的目标端口,第二个参数是二级线程的端口对象,第三个参数的作用是向主线程发送需要的数据,该参数的类型是AnyObject的数组。

创建完消息对象后,要给该消息设置消息id,以便主线程接收后进行判断,最后通过sendBeforeDate:方法发送消息。

线程安全机制

在前文中提到过,在应用中使用多线程势必会给增加我们编写代码的工作量,而且会带来一些潜在的问题,最大的问题就是资源竞争的问题,多个线程同时访问资源或者重复更改资源。如果我们足够幸运,这些问题会使应用产生比较明显的异常现象,那我们尚可发现并修复,但是如果这些问题产生的影响不那么明显,或者说只有在应用做一些特定操作才会发生异常,而我们又没测到时就会给我们带来大麻烦。

或许我们可以让每个线程之间都不进行交互,没个线程都有独有资源,从而避免资源竞争问题的发生,但是这并不是长远之计,很多情况下线程之间必须要进行交互,这时我们就需要更好的设计模式或者工具策略来避免这类问题的发生。所幸的是OS X和iOS系统已经提供了多种线程安全的方法,这一节让我们来看看如何使用它们。

原子操作(Atomic Operations)

原子操作是最简单也是最基本的保证线程安全的方法,原子的本意是不能被分裂的最小粒子,故原子操作是不可被中断的一个或一系列操作。从处理器角度来说原子操作是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址,从应用层面来说就是当一个线程对共享变量进行操作时,其他线程不能对该变量进行操作,并且其他线程不会被阻塞。

举个简单的例子,有一个共享变量i,初始值是1,现在我们对它进行两次i++的操作,期望值是3,但是在多核CPU的情况下就有可能是CPU1对i进行了一次i++操作,CPU2对i进行了一次i++操作,所以结果就并不是我们期望的值3,而是2,因为CPU1和CPU2同时从各自的缓存中读取变量i,分别进行加一操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。在我们使用原子操作时首先应将变量申明为原子类型(atomic_t),然后根据内核提供的原子操作API对变量进行操作,比如给原子类型的变量v增加值i的函数void atomic_add(int i, atomic_t *v);等。OS X和iOS也提供了一些数学运算和逻辑运算的原子操作供我们使用,这里就不深入说明了,大家如果有兴趣可以去官方文档找找。

内存屏障(Memory Barriers)和可见变量(Volatile Variables)

CPU对内存的操作无非就是读和写,我们虽然知道CPU对内存进行了操作,但是我们无法决定在一系列CPU对内存的操作时单个操作指令的顺序,这些顺序完全由CPU随性而来。举个例子,在有两个CPU的情况下,现在有四个指令待操作:

A = 1; x = A;
B = 2; y = B;

这四个指令的执行顺序就可能有24种不同的组合。所以内存屏障就是一个帮助CPU规定操作指令顺序的手段,它将内存操作隔开,给屏障两侧的内存操作强加一个顺序关系,比如所有该屏障之前的写操作和读操作必须在该屏障之后的写操作和读操作之前执行。

可见变量是另一个确保共享变量被多个线程操作后仍能保持正确结果的机制,CPU为了提高处理速度,通常情况下不会直接与主存打交道,而是先将系统主存中的数据读到缓存中,当从缓存中读取到共享变量,对其进行操作后又不会立即写回主存,所以如果其他CPU也要操作该共享变量,就很有可能读到它的旧值。但是当我们在申明共享变量时加上volatile关键字,将其申明为可见变量时就可以避免这种情况,因为CPU从缓存中读取并修改可见共享变量后会立即写回主存,而且其他CPU在操作之前会先判断缓存中的数据是否已过期,如果过期那么从主存中重新缓存,这样一来可见变量在每个CPU操作时都能保证是最新值。但需要注意的是内存屏障和可见变量都会降低编译器的性能,所以没有必须要使用的情况时不要滥用这两个机制。

锁机制

锁机制在大多数编程语言中都是很常用的线程安全机制,你可以在关键的代码前后,或者只希望同时只能被一个线程执行的任务前后加上线程锁来避免因为多线程给程序造成不可预知的问题。OS X和iOS提供了多种锁的类型,下面让我们来看一看:

  • 互斥锁(Mutex):互斥锁扮演的角色就是代码或者说任务的栅栏,它将你希望保护的代码片段围起来,当其他线程也试图执行这段代码时会被互斥锁阻塞,直到互斥锁被释放,如果多个线程同时竞争一个互斥锁,有且只有一个线程可以获得互斥锁。
  • 递归锁(Recursive lock):递归锁是互斥锁的变种。它允许一个线程在已经拥有一个锁,并且没有释放的前提下再次获得锁。当该线程释放锁时也需要一个一个释放。
  • 读写锁(Read-write lock):读写锁一般用在有资源被多个线程频繁的进行读操作,而只偶尔会有专职线程对该资源进行写操作的情况下。读写锁可被多个进行读操作的线程获得,但只能被一个进行写操作的线程获得,当有读操作的线程等待时,写操作的线程就不能获得锁,反之亦然,当写操作的线程在等待时,读操作的线程就不能获得锁。
  • 分配锁(Distributed lock):这种锁作用在进程级别,将进程保护起来,但是该锁不会阻塞其他进程,而是当其他进程与被保护进程交互时分配锁会告知前来的访问进程被访问进程处于锁状态,让前来访问的进程自行决定下一个操作。
  • 自旋锁(Spin lock):自旋锁与互斥锁有点类似,但不同的是其他线程不会被自旋锁阻塞,而是而是在进程中空转,就是执行一个空的循环。一般用于自旋锁被持有时间较短的情况。
  • 双检测锁(Double-checked lock):这种锁的目的是为了最大限度推迟上锁的时间,因为在多线程中线程安全对开销还是挺大的,所以一般能不上锁就不上锁。所以这种锁在上锁之前会先检查一次是否需要上锁,在上锁之后再检查一次,最后才真正执行操作。

Conditions

Conditions是一种多线程间协调通信的机制,它通常用于标明共享资源是否可被访问或者确保一系列任务能按照指定的执行顺序执行。如果一个线程试图访问一个共享资源,而正在访问该资源的线程将其条件设置为不可访问,那么该线程会被阻塞,直到正在访问该资源的线程将访问条件更改为可访问状态或者说给被阻塞的线程发送信号后,被阻塞的线程才能正常访问这个资源。后面会说明如何使用这种机制。

设计线程安全需要注意的事项

诚然使用线程安全的各种机制可以是我们的程序更加健壮,不易出错,但是因为这些机制本身也会有较大的性能开销,如果滥用这些机制反而会严重影响到程序的性能。所以我们应该在线程安全和性能之间寻求到一个平衡点,这一节我们就来看看在设计线程安全时应该注意的事项。

避免滥用线程安全机制

不论是新的项目还是已经有的项目,在设计逻辑代码或者属性时应该避免产生线程安全与不安全的问题。有效的避免措施就是减少逻辑代码之间的交互,或者说任务与任务之间的交互,线程与线程之间的交互,减少多线程中任务访问同一变量的情况,如果需要那么可以确保每个任务中都有该变量的拷贝,这样就可以有效避免对变量或者任务采取线程安全机制。虽然对变量进行拷贝也会消耗资源,但是我们应该要判断一下这与采用线程安全机制消耗的资源之间谁多谁少,从而做出正确的决定。

认清使用线程安全机制时的陷阱

在使用锁机制和内存屏障机制时我们往往需要考虑将它们设置在代码的哪个位置是最正确的,但是有些时候,你认为正确的位置不代表它真的正确,下面是一段伪代码片段,向我们揭示一个使用锁机制时容易发生的陷阱。假设有一个可变类型的数组myArray,但是该数组中的对象是不可变类型的对象anObject

NSLock* arrayLock = GetArrayLock(); 

NSMutableArray* myArray = GetSharedArray();

id anObject;

[arrayLock lock];

anObject = [myArray objectAtIndex:0];

[arrayLock unlock];

[anObject doSomething];

上述代码片段中,对从myArray数组中获取第一个元素的操作加了锁,因为该数组是可变类型的,所以加锁防止其他线程同时操作该数组从而导致错误发生,又因为anObject是一个不可变类型对象,所以不需要担心其他线程会对其进行改变,所以调用anObject对象的doSomething方法时并没有加锁。

看起来这段代码的逻辑似乎没什么问题,但是凡事都架不住如果和万一,如果在arrayLock释放锁之后和anObject对象调用doSomething方法之前这区间里,另外一个线程清空了myArray里的元素,这时这段代码的结果会怎样呢?答案显然是因为当前类对anObject对象的引用被释放,anObject对象因为指向了错误的内存地址从而调用方法出错。所以为了避免这种小概率事件的发生,应该将anObject对象调用方法的操作也加上锁:

NSLock* arrayLock = GetArrayLock(); 

NSMutableArray* myArray = GetSharedArray();

id anObject;

[arrayLock lock];

anObject = [myArray objectAtIndex:0];

[anObject doSomething];

[arrayLock unlock];

那么问题又来了,如果doSomething方法执行的时间很长,线程锁一直无法释放,那么又会对线程的性能产生很大影响。要想彻底解决问题,就要找到产生问题的关键点,在这个示例中产生问题的关键点就是anObject对象有可能被其他线程释放,所以解决问题的关键就是防止anObject对象被释放,我们来看看最终的解决方案:

NSLock* arrayLock = GetArrayLock(); 

NSMutableArray* myArray = GetSharedArray();

id anObject;

[arrayLock lock];

anObject = [myArray objectAtIndex:0];

[anObject retain];

[arrayLock unlock];

[anObject doSomething];

[anObject release];

防止死锁和活锁的发生

死锁的意思就是线程A和线程B各持有一把锁,现在线程A在等待线程B释放锁,而线程B又在等待线程A释放锁,所以这两个线程谁也拿不到锁,也不是释放自己持有的锁,就会永远被阻塞在进程中。

活锁的意思是线程A可以使用资源,但它很礼貌,让其他线程先使用资源,线程B也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源,导致活锁,活锁与死锁的区别在于前者的线程并没有被阻塞,而是在不停的做一些与任务无关的事。

产生死锁和活锁的根本原因是线程中持有多把锁,所以避免这两种情况发生的最好办法就是尽量让线程只持有一把锁,如果实在有需求要持有多把锁,那么也应该尽量避免其他线程来请求锁。

正确使用volatile关键字

如果你已经使用的锁机制来保护一段代码逻辑,那么就不要使用volatile关键字来保护这段代码中使用的变量。上文中说过,可见变量机制会让代码每次从主存中加载读取变量而非缓存,本身就比较影响性能,如果再与锁机制结合,不但没有起到额外的保护作用,反而会严重影响程序的性能。所以如果使用了锁机制,那么可以完全省去使用可见变量机制,因为锁机制就已经可以很好的保护变量的线程安全性了,不需要多此一举。

使用原子操作

有些时候我们只希望一些数学运算或者简单的逻辑能够保证线程安全,如果使用锁机制或者条件机制虽然可以实现,但是会耗费较大的资源开销,并且锁机制还会使线程阻塞,造成性能损失,非常不划算,所以当遇到这种情况时,我们可以尝试使用原子操作来达到目的。

我们一般使用原子操作对32位和64位的值执行一些数学运算或简单的逻辑运算,主要依靠底层的硬件指令或者使用内存屏障确保正在执行的操作是线程安全的,下面我们来看看Apple给我们提供了哪些原子操作的方法:

Add操作

Add操作是将两个整数相加,并将结果存储在其中一个变量中:

  • OSAtomicAdd32(__theAmount: Int32, _ __theValue: UnsafeMutablePointer<Int32>) -> Int32
  • OSAtomicAdd32Barrier(__theAmount: Int32, _ __theValue: UnsafeMutablePointer<Int32>) -> Int32
  • OSAtomicAdd64(__theAmount: Int64, _ __theValue: UnsafeMutablePointer<Int64>) -> Int64
  • OSAtomicAdd64Barrier(__theAmount: Int64, _ __theValue: UnsafeMutablePointer<Int64>) -> Int64
var num: Int64 = 10

OSAtomicAdd64(20, &num)

OSAtomicAdd64Barrier(20, &num)

print("\(num)") // 50

Increment操作

Increment操作将指定值加1:

  • OSAtomicIncrement32(__theValue: UnsafeMutablePointer<Int32>) -> Int32
  • OSAtomicIncrement32Barrier(__theValue: UnsafeMutablePointer<Int32>) -> Int32
  • OSAtomicIncrement64(__theValue: UnsafeMutablePointer<Int64>) -> Int64
  • OSAtomicIncrement64Barrier(__theValue: UnsafeMutablePointer<Int64>) -> Int64
var num: Int64 = 10

OSAtomicIncrement64(&num)

OSAtomicIncrement64Barrier(&num)

print("\(num)") // 12

Decrement操作

Decrement操作将指定值减1:

  • OSAtomicDecrement32(__theValue: UnsafeMutablePointer<Int32>) -> Int32
  • OSAtomicDecrement32Barrier(__theValue: UnsafeMutablePointer<Int32>) -> Int32
  • OSAtomicDecrement64(__theValue: UnsafeMutablePointer<Int64>) -> Int64
  • OSAtomicDecrement64Barrier(__theValue: UnsafeMutablePointer<Int64>) -> Int64
var num: Int64 = 10

OSAtomicDecrement64(&num)

OSAtomicDecrement64Barrier(&num)

print("\(num)") // 8

OR逻辑运算、AND逻辑运算、XOR逻辑运算

对两个32位数值中的位置相同的位执行按位比较:

  • OSAtomicOr32(__theMask: UInt32, _ __theValue: UnsafeMutablePointer<UInt32>) -> Int32
  • OSAtomicOr32Barrier(__theMask: UInt32, _ __theValue: UnsafeMutablePointer<UInt32>) -> Int32
  • OSAtomicAnd32(__theMask: UInt32, _ __theValue: UnsafeMutablePointer<UInt32>) -> Int32
  • OSAtomicAnd32Barrier(__theMask: UInt32, _ __theValue: UnsafeMutablePointer<UInt32>) -> Int32
  • OSAtomicXor32(__theMask: UInt32, _ __theValue: UnsafeMutablePointer<UInt32>) -> Int32
  • OSAtomicXor32Barrier(__theMask: UInt32, _ __theValue: UnsafeMutablePointer<UInt32>) -> Int32

CAS操作

CAS操作是比较与交换(Compare and Swap)操作,有三个参数分别是旧值、新值、想要比较的值的内存地址,整个过程是先将你期望的旧值与指定的内存地址中的值进行比较,如果相同,那么将该内存地址的值更新为指定的新值,并返回true,如果比较后发现不同,那么不再做任何操作,并返回false,Apple提供了不同类型的CAS原子操作:

  • OSAtomicCompareAndSwap32(__oldValue: Int32, _ __newValue: Int32, _ __theValue: UnsafeMutablePointer<Int32>) -> Bool
  • OSAtomicCompareAndSwap64(__oldValue: Int64, _ __newValue: Int64, _ __theValue: UnsafeMutablePointer<Int64>) -> Bool
  • OSAtomicCompareAndSwapPtr(__oldValue: UnsafeMutablePointer<Void>, _ __newValue: UnsafeMutablePointer<Void>, _ __theValue: UnsafeMutablePointer<UnsafeMutablePointer<Void>>) -> Bool
  • OSAtomicCompareAndSwapLong(__oldValue: Int, _ __newValue: Int, _ __theValue: UnsafeMutablePointer<Int>) -> Bool
var num: Int64 = 10

let result = OSAtomicCompareAndSwap64(10, 20, &num)

print("\(num)") // 20

print(result) // true


var num: Int64 = 10

let result = OSAtomicCompareAndSwap64(11, 20, &num)

print("\(num)") // 10

print(result) // false

比特位设置操作

将给定比特位的值设置位1或者0:

  • OSAtomicTestAndSet(__n: UInt32, _ __theAddress: UnsafeMutablePointer<Void>) -> Bool
  • OSAtomicTestAndSetBarrier(__n: UInt32, _ __theAddress: UnsafeMutablePointer<Void>) -> Bool
  • OSAtomicTestAndClear(__n: UInt32, _ __theAddress: UnsafeMutablePointer<Void>) -> Bool
  • OSAtomicTestAndClearBarrier(__n: UInt32, _ __theAddress: UnsafeMutablePointer<Void>) -> Bool

使用锁机制

锁机制是多线程编程中最常用的也是最基本的确保线程安全的机制,它能有效的保证多行逻辑代码的线程安全性。OS X和iOS系统为我们提供了基本的互斥锁和基于互斥锁变异的特殊锁以应对不同的情况。这一节我们来看看如何使用锁机制。

POSIX互斥锁

前文中说过,POSIX是可移植操作系统接口(Portable Operating System Interface of UNIX),它定义了操作系统应该为应用程序提供的接口标准,在类Unix系统中都可以使用。使用POSIX互斥锁很简单,先申明互斥锁指针,类型为UnsafeMutablePointer<pthread_mutex_t>,然后通过pthread_mutex_init函数初始化互斥锁,最后通过pthread_mutex_lock函数和pthread_mutex_unlock函数上锁和释放锁:

class TestLock {

let mutex: UnsafeMutablePointer<pthread_mutex_t>

init() {

mutex = UnsafeMutablePointer.alloc(sizeof(pthread_mutex_t))

}


func posixMutexLock() {

pthread_mutex_init(mutex, nil)

pthread_mutex_lock(mutex)

print("Do work...")

pthread_mutex_unlock(mutex)

}

}

let textLock = TestLock()
textLock.posixMutexLock()

使用NSLock

在Cocoa框架中,我们可以使用NSLock来实现锁机制,该类遵循了NSLocking协议,并实现了加锁和释放锁的方法。

NSLock中有两个加锁的方法:

  • tryLock:该方法使当前线程试图去获取锁,并返回布尔值表示是否成功,但是当获取锁失败后并不会使当前线程阻塞。
  • lockBeforeDate:该方法与上面的方法类似,但是只有在设置的时间内获取锁失败线程才不会被阻塞,如果获取锁失败时已超出了设置的时间,那么当前线程会被阻塞。
class TestLock {

let nslock: NSLock

init() {

nslock = NSLock()

}

func acquireLock() {

nslock.tryLock()

// nslock.lockBeforeDate(NSDate(timeIntervalSinceNow: 10))

print("Do work...")

nslock.unlock()

}

}

let textLock = TestLock()
textLock.acquireLock()

使用NSRecursiveLock

上文中介绍了几种锁的类型,其中一种叫递归锁,在Cocoa中对应的类是NSRecursiveLock,我们来看看如何使用:

class TestLock {

let nsRecursiveLock: NSRecursiveLock

init() {

nsRecursiveLock = NSRecursiveLock()

}

func recursiveFunction(var value: Int) {

nsRecursiveLock.lock()

if value != 0 {

--value

print("\(value)")

recursiveFunction(value)

}

nsRecursiveLock.unlock()

}

}

let textLock = TestLock()
textLock.recursiveFunction(5)

使用NSConditionLock

条件锁也是互斥锁的一种变种,在Cocoa框架中对应的类是NSConditionLock,条件锁顾名思义可以设置加锁和释放锁的条件。假设我们有一个消息队列,并且有消息生产者和消息消费者,那么一般情况是当消息生产者产生消息,放入消息队列,然后消息消费者从消息队列中获取消息,并将其从消息队列移除进行后续操作。那么消费者在获取消息和移除消息时要确保两点先决条件,第一就是获取消息时队列中确实已有消息,第二就是此时生产者不能向队列中添加消息,否则会影响消息队列中消息的顺序或者影响获取到消息的结果,所以在这种情况下我们就可以使用条件锁来保证他们的线程安全:

class TestLock {

let nsConditionLock: NSConditionLock
var messageQueue = [AnyObject]()
let HAS_MESSAGES = 1
let NO_MESSAGES = 0

init() {

nsConditionLock = NSConditionLock(condition: NO_MESSAGES)

}

func produceMessage() {

NSThread.detachNewThreadSelector("consumeMessage", toTarget: self, withObject: nil)

while true {

nsConditionLock.lock()

// 生产消息并添加到消息队列中

nsConditionLock.unlockWithCondition(HAS_MESSAGES)

}

}

func consumeMessage() {

while true {

nsConditionLock.lockWhenCondition(HAS_MESSAGES)

// 从消息队列中获取消息并从队列中移除消息

nsConditionLock.unlockWithCondition(messageQueue.isEmpty ? NO_MESSAGES : HAS_MESSAGES)

}

}

}

let textLock = TestLock()
textLock.produceMessage()

使用@synchronized关键字

在Objective-C中,我们会经常使用@synchronized关键字来修饰变量,确保变量的线程安全,它能自动为修饰的变量创建互斥锁或解锁:

- (void)myMethod:(id)anObj { 

@synchronized(anObj) {

// 在该作用域中,anObj不会被其他线程改变

}

}

从上面的代码片段中可以看到myMethod:方法的anObj参数在被@synchronized关键字修饰的作用域中是线程安全的。而且使用该关键字还有一个好处,那就是当有多个线程要同时执行一个带参数的方法,但不同线程中传递的参数不同,如果用NSLock将该方法中的逻辑代码上锁,那么就只能有一个线程获得锁,而其他线程就会被阻塞,如果使用@synchronized关键字就可以避免其他线程被阻塞的情况。

但在Swift中,Apple不知出于什么考虑,这个关键字已经不存在了,也就是我们不能在Swift中使用这个关键字对变量加锁了,但关键字都是语法糖,虽然不能使用语法糖,但还是可以使用其背后的机制的,我们来看看objc_sync的源码,看看这个关键字都干了些什么:

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;

if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}

return result;
}


// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;

if (obj) {
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
}


return result;
}

可见@synchronized关键字其实是调用了objc_sync_enterobjc_sync_exit这两个方法,所以在Swift中使用时可以这样给变量加锁:

func myMethod(anObj: AnyObject!) {

objc_sync_enter(anObj)

// anObj参数在这两个方法之间具有线程安全特性,不会被其他线程改变

objc_sync_exit(anObj)

}

使用Condition机制

Condition机制和锁机制很类似,区别也不大,同样都会使线程阻塞,这一节我们来看看如何使用该机制。

使用NSCondition类

这里举个生产者和消费者的例子,消费者从队列中获取产品进行消费,当队列中没有产品时消费者等待生产者生产,当生产者生产出产品放入队列后再通知消费者继续进行消费:

class TestLock {

var products: [AnyObject]
let nscondition: NSCondition

init() {

products = [AnyObject]()

nscondition = NSCondition()

NSThread.detachNewThreadSelector("consumeProduct", toTarget: self, withObject: nil)

NSThread.detachNewThreadSelector("generateProduct", toTarget: self, withObject: nil)

}

func consumeProduct() {

nscondition.lock()

guard products.count == 0 else {

nscondition.wait()

}

let product = products[0]

products.removeAtIndex(0)

print("消费产品")

nscondition.unlock()

}

func generateProduct() {

nscondition.lock()

let product = NSObject()

products.append(product)

print("生产产品")

nscondition.signal()

nscondition.unlock()

}

}

从上面代码中可以看到,NSCondition类同样是用lockunlock方法进行上锁和释放锁,然后通过wait方法阻塞线程,通过signal方法唤醒阻塞的线程,该方法唤醒的时最近一次使用wait方法等待的线程。如果想一次性唤醒所有在等待的线程,可以使用broadcast方法。NSCondition还有另外一个阻塞线程的方法waitUntilDate(_ limit: NSDate),该方法设置一个线程阻塞时间并返回一个布尔值,如果在指定的时间内没有信号量的通知,那么就唤醒线程继续进行,此时该方法返回false,如果在指定时间内接收到信号量的通知,此时该方法返回true