读 Threading Programming Guide 笔记(四)

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

分享到: