https://www.mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html
https://swift.gg/2018/06/07/friday-qa-2015-02-06-locks-thread-safety-and-swift/
在 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
是一个阻塞递归锁。
值类型
注意,pthread_mutex_t
,pthread_rwlock_t
和 OSSpinLock
是值类型,而不是引用类型。这意味着如果你用 =
进行赋值操作,实际上会复制一个副本。这会造成严重的后果,因为这些类型无法复制!如果你不小心复制了它们中的任意一个,这个副本无法使用,如果使用可能会直接崩溃。这些类型的 pthread
函数会假定它们的内存地址与初始化时一样,因此如果将它们移动到其他地方就可能会出问题。OSSpinLock
不会崩溃,但复制操作会生成一个完全独立的锁,这不是你想要的。
如果使用这些类型,就必须注意不要去复制它们,无论是显式的使用 =
操作符还是隐式地操作。
例如,将它们嵌入到结构中或在闭包中捕获它们。
另外,由于锁本质上是可变对象,需要用 var
来声明它们。
其他锁都是是引用类型,它们可以随意传递,并且可以用 let
声明。
初始化
2015-02-10 更新:本节中所描述的问题已经以惊人的速度被淘汰。苹果昨天发布了 Xcode 6.3 beta 1,其中包括 Swift 1.2。在其他更改中,现在使用一个空的初始化器导入 C 结构,将所有字段设置为零。简而言之,你现在可以直接使用 pthread_mutex_t()
,不需要下面提到的扩展。
pthread 类型很难在 swift 中使用。它们被定义为不透明的结构体中包含了一堆存储变量,例如:
struct _opaque_pthread_mutex_t { |
目的是声明它们,然后使用 init 函数对它们进行初始化,使用一个指针存储和填充。在 C 中,它看起来像:
pthread_mutex_t mutex; |
这段代码可以正常的工作,只要你记得调用 pthread_mutex_init
。然而,Swift 真的真的不喜欢未初始化的变量。与上面代码等效的 Swift 版本无法编译:
var mutex: pthread_mutex_t |
Swift 需要变量在使用前初始化。pthread_mutex_init
不使用传入的变量的值,只是重写它,但是 Swift 不知道,因此它产生了一个错误。为了满足编译器,变量需要用某种东西初始化。在类型之后使用 ()
,但这样写仍然会报错:
var mutex = pthread_mutex_t() |
Swift 需要那些不透明字段的值。__sig
可以传入零,但是 __opaque
就有点烦人了。下面的结构体需要桥接到 swift 中:
struct _opaque_pthread_mutex_t { |
目前没有简单的方法使用一堆 0 构建一个元组,只能像下面这样把所有的 0 都写出来:
var mutex = pthread_mutex_t(__sig: 0, |
这么写太难看了,但我没找到好的方法。我能想到最好的做法就是把它写到一个扩展中,这样直接使用空的 ()
就可以了。下面是我写的两个扩展:
extension pthread_mutex_t { |