Swift 中的锁和线程安全
Posted SwiftGG翻译组
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift 中的锁和线程安全相关的知识,希望对你有一定的参考价值。
译者:Lefe_x;校对:numbbbbb,Yousanflics,liberalism;定稿:CMB
在 Swift 中有个有趣的现象:它没有与线程相关的语法,也没有明确的互斥锁/锁(mutexes/locks
)概念,甚至 Objective-C 中有的 @synchronized
和原子属性它都没有。幸运的是,苹果系统的 API 可以非常容易地应用到 Swift 中。今天,我会介绍这些 API 的用法以及从 Objective-C 过渡的一些问题,这些灵感都来源于 Cameron Pulsford。
快速回顾一下锁
锁(lock
)或者互斥锁(mutex
)是一种结构,用来保证一段代码在同一时刻只有一个线程执行。它们通常被用来保证多线程访问同一可变数据结构时的数据一致性。主要有下面几种锁:
阻塞锁(
Blocking locks
):常见的表现形式是当前线程会进入休眠,直到被其他线程释放。自旋锁(
Spinlocks
):使用一个循环不断地检查锁是否被释放。如果等待情况很少话这种锁是非常高效的,相反,等待情况非常多的情况下会浪费 CPU 时间。读写锁(
Reader/writer locks
):允许多个读线程同时进入一段代码,但当写线程获取锁时,其他线程(包括读取器)只能等待。这是非常有用的,因为大多数数据结构读取时是线程安全的,但当其他线程边读边写时就不安全了。递归锁(
Recursive locks
):允许单个线程多次获取相同的锁。非递归锁被同一线程重复获取时可能会导致死锁、崩溃或其他错误行为。
APIs
苹果提供了一系列不同的锁 API,下面列出了其中一些:
pthread_mutex_t
pthread_rwlock_t
dispatch_queue_t
NSOperationQueue
当配置为serial
时NSLock
OSSpinLock
除此之外,Objective-C 提供了 @synchronized
语法结构,它其实就是封装了 pthread_mutex_t
。与其他 API 不同的是,@synchronized
并未使用专门的锁对象,它可以将任意 Objective-C 对象视为锁。@synchronized(someObject)
区域会阻止其他 @synchronized(someObject)
区域访问同一对象指针。不同的 API 有不同的行为和能力:
pthread_mutex_t
是一个可选择性地配置为递归锁的阻塞锁;pthread_rwlock_t
是一个阻塞读写锁;dispatch_queue_t
可以用作阻塞锁,也可以通过使用 barrier block 配置一个同步队列作为读写锁,还支持异步执行加锁代码;NSOperationQueue
可以用作阻塞锁。与dispatch_queue_t
一样,支持异步执行加锁代码。NSLock
是 Objective-C 类的阻塞锁,它的同伴类NSRecursiveLock
是递归锁。OSSpinLock
顾名思义,是一个自旋锁。
最后,@synchronized
是一个阻塞递归锁。
值类型
如果使用这些类型,就必须注意不要去复制它们,无论是显式的使用 =
操作符还是隐式地操作。
例如,将它们嵌入到结构中或在闭包中捕获它们。
另外,由于锁本质上是可变对象,需要用 var
来声明它们。
其他锁都是是引用类型,它们可以随意传递,并且可以用 let
声明。
初始化
2015-02-10 更新:本节中所描述的问题已经以惊人的速度被淘汰。苹果昨天发布了 Xcode 6.3 beta 1,其中包括 Swift 1.2。在其他更改中,现在使用一个空的初始化器导入 C 结构,将所有字段设置为零。简而言之,你现在可以直接使用 pthread_mutex_t()
,不需要下面提到的扩展。
pthread 类型很难在 swift 中使用。它们被定义为不透明的结构体中包含了一堆存储变量,例如:
struct _opaque_pthread_mutex_t {
long __sig;
char __opaque[__PTHREAD_MUTEX_SIZE__];
};
目的是声明它们,然后使用 init 函数对它们进行初始化,使用一个指针存储和填充。在 C 中,它看起来像:
c
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
这段代码可以正常的工作,只要你记得调用 pthread_mutex_init
。然而,Swift 真的真的不喜欢未初始化的变量。与上面代码等效的 Swift 版本无法编译:
var mutex: pthread_mutex_t
pthread_mutex_init(&mutex, nil)
// error: address of variable 'mutex' taken before it is initialized
Swift 需要变量在使用前初始化。pthread_mutex_init
不使用传入的变量的值,只是重写它,但是 Swift 不知道,因此它产生了一个错误。为了满足编译器,变量需要用某种东西初始化。在类型之后使用 ()
,但这样写仍然会报错:
var mutex = pthread_mutex_t()
// error: missing argument for parameter '__sig' in call
Swift 需要那些不透明字段的值。__sig
可以传入零,但是 __opaque
就有点烦人了。下面的结构体需要桥接到 swift 中:
struct _opaque_pthread_mutex_t {
var __sig: Int
var __opaque: (Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8,
Int8, Int8, Int8, Int8)
}
目前没有简单的方法使用一堆 0 构建一个元组,只能像下面这样把所有的 0 都写出来:
var mutex = pthread_mutex_t(__sig: 0,
__opaque: (0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0))
这么写太难看了,但我没找到好的方法。我能想到最好的做法就是把它写到一个扩展中,这样直接使用空的 ()
就可以了。下面是我写的两个扩展:
extension pthread_mutex_t {
init() {
__sig = 0
__opaque = (0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0)
}
}
extension pthread_rwlock_t {
init() {
__sig = 0
__opaque = (0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0)
}
}
可以通过下面这种方式使用:
var mutex = pthread_mutex_t()
pthread_mutex_init(&mutex, nil)
锁的封装
为了使这些不同的 API 更易于使用,我编写了一系列小型函数。我决定把 with
作为一个方便、简短、看起来像语法的名字,灵感来自 python 的 with
声明。Swift 函数重载允许不同类型使用相同的名称。基本形式如下所示:
func with(lock: SomeLockType, f: Void -> Void) { ...
然后在锁定的情况下执行函数 f。下面我们来实现这些类型。
对于值类型,它需要一个指向锁的指针,以便 lock/unlock 函数可以修改它。这个实现pthread_mutex_t
只是调用相应的 lock 和 unlock 函数,f 函数在两者之间调用:
func with(mutex: UnsafeMutablePointer<pthread_mutex_t>, f: Void -> Void) {
pthread_mutex_lock(mutex)
f()
pthread_mutex_unlock(mutex)
}
pthread_rwlock_t
的实现几乎完全相同:
func with(rwlock: UnsafeMutablePointer<pthread_rwlock_t>, f: Void -> Void) {
pthread_rwlock_rdlock(rwlock)
f()
pthread_rwlock_unlock(rwlock)
}
与读写锁做个对比,它们看起来很像:
func with_write(rwlock: UnsafeMutablePointer<pthread_rwlock_t>, f: Void -> Void) {
pthread_rwlock_wrlock(rwlock)
f()
pthread_rwlock_unlock(rwlock)
}
dispatch_queue_t
更简单。它只需要封装 dispatch_sync:
:
func with(queue: dispatch_queue_t, f: Void -> Void) {
dispatch_sync(queue, f)
}
如果一个人想显摆自己很聪明,那么可以充分利用 Swift 的函数式特性简单的写出这样的代码:
let with = dispatch_sync
这种写法存在一些问题,最大的问题是它会和我们这里使用的基于类型的重载混淆。
NSOperationQueue
在概念上是相似的,不过没有 dispatch_sync
可以用。我们需要创建一个操作(operation
),将其添加到队列中,并显式等待它完成:
func with(opQ: NSOperationQueue, f: Void -> Void) {
let op = NSBlockOperation(f)
opQ.addOperation(op)
op.waitUntilFinished()
}
实现 NSLock
看起来像 pthread
版本,只是锁定调用有些不同:
func with(lock: NSLock, f: Void -> Void) {
lock.lock()
f()
lock.unlock()
}
最后,OSSpinLock
的实现同样也是如此:
func with(spinlock: UnsafeMutablePointer<OSSpinLock>, f: Void -> Void) {
OSSpinLockLock(spinlock)
f()
OSSpinLockUnlock(spinlock)
}
模仿 @synchronized
有了上面的封装,模仿 @synchronized
的实现就变得很简单。给你的类添加一个属性,持有一个锁,然后使用 with
替代 @synchronized
:
let queue = dispatch_queue_create("com.example.myqueue", nil)
func setEntryForKey(key: Key, entry: Entry) {
with(queue) {
entries[key] = entry
}
}
从 block 中获取数据比较麻烦。@synchronized
可以从内部 return
,但是 with
做不到。你必须使用一个 var
变量在 block 内部赋值给它:
func entryForKey(key: Key) -> Entry? {
var result: Entry?
with(queue) {
result = entries[key]
}
return result
}
按理说可以将这段代码当做模板封装在一个通用函数中,但是它无法通过 Swift 编译器的类型推断,目前还没有找到解决方法。
模拟原子属性
原子属性(atomic
)并不常用。与其他代码属性不同,原子属性并不支持组合率。如果函数 f 不存在内存泄漏,函数 g 不存在内存泄漏,那么函数 h 只是调用 f 和 g 也不会存在内存泄漏。但是原子属性并不满足这个条件。举一个例子,假设你有一个定义成原子属性并且线程安全的 Account 类:
let checkingAccount = Account(amount: 100)
let savingsAccount = Account(amount: 0)
现在要把钱转到储蓄账户中:
checkingAccount.withDraw(100)
savingsAccount.deposit(100)
在另一个线程中,统计并显示余额:
println("Your total balance is: \(checkingAccount.amount + savingsAccount.amount)")
在某些情况下,这段代码会打印 0,而不是 100,尽管事实上这些 Account 对象本身是原子属性,并且用户确实有 100 的余额。所以,最好让整个子系统都满足原子性,而不是单个属性。
在极少数情况下,原子属性是有用的,因为它并不依赖其他特性,只需要线程安全即可。要在 Swift 中实现这一点,需要一个计算属性来完成锁定,用另一个常规属性保存值:
private let queue = dispatch_queue_create("...", nil)
private var _myPropertyStorage: SomeType
var myProperty: SomeType {
get {
var result: SomeType?
with(queue) {
result = _myPropertyStorage
}
return result!
}
set {
with(queue) {
_myPropertyStorage = newValue
}
}
}
如何选择锁 API
pthread
API 在 Swift 中不太好用,而且功能并不比其它 API 多。一般我比较喜欢在 C 和 Objective-C 中使用它们,因为它们又好用又高效。但是在 Swift 中,除非必要,我一般不会用。
一般来说不需要用读写锁,大多数情况下读写速度都非常快。读写锁带来的额外开销超过了并发读取带来的效率提升。
递归锁会发生死锁。多数情况下它们是有用的,但如果你发现自己需要获取一个已经在当前线程被锁住的锁,那最好重新设计代码,通常来说不会出现这种需求。
我的建议是,如果不知道该用什么,那就默认选择 dispatch_queue_t
。虽然用起来相对麻烦,但是不会产生太多问题。该 API 非常方便,并且确保你永远不会忘记调用 lock 和 unlock。它提供了许多有用的 API,如使用单个 dispatch_async
在后台执行被锁定的代码,或者设置定时器或其他作用于 queue 的事件源,以便它们自动执行锁定。你甚至可以用它作为 NSNotificationCenter
观察者,或者使用 NSOperationQueue
的属性 underlyingQueue
作为 NSURLSession
代理。
NSOperationQueue
可能认为自己和 dispatch_queue_t
一样牛
以上是关于Swift 中的锁和线程安全的主要内容,如果未能解决你的问题,请参考以下文章