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 中的锁和线程安全的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程:Java中的锁和线程同步机制

多线程中的锁

一文足以了解什么是 Java 中的锁.

一文足以了解什么是 Java 中的锁.

Objective-c 中的锁

Linux 线程安全常用的锁机制