译文 · 原文: Friday Q&A 2015-02-06: Locks, Thread Safety, and Swift · 作者 Mike Ash
原文:https://www.mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html 发布:2015-02-06 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样
Swift 的一个有趣方面是,语言中没有任何与 threading(线程)相关的内容,更具体地说,没有 mutexes/locks(互斥锁/锁)。即使 Objective-C 也有 @synchronized 和 atomic properties(原子属性)。幸运的是,大部分功能都在平台 API 中,这些 API 在 Swift 中易于使用。今天我将探讨这些 API 的使用以及从 Objective-C 的过渡,这是由 Cameron Pulsford 建议的主题。
A Quick Recap on Locks 锁的快速回顾
锁,或 mutex(互斥锁),是一种确保在任何给定代码区域中一次只有一个 thread(线程)处于活动状态的构造。它们通常用于确保访问 mutable data structure(可变数据结构)的多个线程都能看到一致的视图。有几种类型的锁:
-
Blocking locks(阻塞锁)会让线程休眠,同时它等待另一个线程释放锁。这是通常的行为。
-
Spinlocks(自旋锁)使用忙循环来不断检查锁是否已被释放。如果等待很少发生,这更有效率,但如果等待常见,则会浪费 CPU 时间。
-
读写锁(Reader / writer locks)允许多个 “读取者” 线程同时进入临界区,但当 “写入者” 线程获取锁时,会排斥所有其他线程(包括读取者)。这很有用,因为许多数据结构在多线程同时读取时是安全的,但在其他线程正在读取或写入时进行写入则不安全。
-
递归锁(Recursive locks)允许单个线程多次获取同一把锁。非递归锁在同一线程重新进入时,可能导致死锁、崩溃或其他异常行为。
APIs 苹果提供了一大批不同的互斥锁机制。以下是冗长但不全面的列表:
pthread_mutex_t(线程互斥锁)。pthread_rwlock_t(线程读写锁)。dispatch_queue_t(调度队列)。- 当配置为串行时的
NSOperationQueue(操作队列)。 NSLock(锁)。OSSpinLock(自旋锁)。(译注:OSSpinLock在现代系统中因公平性问题已被废弃,不建议使用。)
此外,Objective-C 提供了 @synchronized 语言构造,目前其底层基于 pthread_mutex_t 实现。与其他同步机制不同,@synchronized 不使用显式的锁对象,而是将任意一个 Objective-C 对象视为锁。一个 @synchronized(someObject) 代码块将会阻塞任何使用相同对象指针的其他 @synchronized 代码块的访问。这些不同的工具各自具有不同的行为和能力:
pthread_mutex_t是一个阻塞锁,可选择性地配置为递归锁。pthread_rwlock_t是一个阻塞的读写锁。dispatch_queue_t可用作阻塞锁。通过将其配置为并发队列(concurrent queue)并使用 barrier blocks,它可以用作读写锁。它还支持对锁定区域进行异步执行。NSOperationQueue可用作阻塞锁。与dispatch_queue_t类似,它也支持对锁定区域进行异步执行。NSLock是一个阻塞锁,以 Objective-C 类的形式呈现。其伴随类NSRecursiveLock如其名称所示,是一个递归锁。OSSpinLock如其名称所示,是一个自旋锁。
最后,@synchronized 是一种阻塞递归锁(blocking recursive lock)。
值类型
需要注意,pthread_mutex_t、pthread_rwlock_t 以及 OSSpinLock 是值类型(value types),而非引用类型(reference types)。这意味着对它们使用 = 运算符会产生一份副本。这一点至关重要,因为这些类型无法被复制!如果你复制了其中一个 pthread 类型,该副本将无法使用,并且在你尝试使用它时可能导致崩溃。处理这些类型的 pthread 函数假定值位于其初始化时的相同内存地址,事后将它们移动到其他地方是个坏主意。OSSpinLock 虽然不会崩溃,但你会得到一个完全独立的锁,而这绝非你想要的。
如果你使用这些类型,必须小心绝不要复制它们,无论是通过 = 运算符显式复制,还是通过例如将它们嵌入结构体或在闭包中捕获等隐式方式复制。
此外,由于锁本质上是可变对象(mutable objects),这意味着你需要用 var 而非 let 来声明它们。
其他的则是引用类型(reference types),可以随意传递,并且可以用 let 声明。
更新于 2015-02-10:本节所述问题已随着 Xcode 6.3 beta 1 的发布而迅速过时。该版本包含了 Swift 1.2,其中 C 结构体现在被导入时会带有一个将所有字段设为零的空初始化器。简而言之,现在可以直接编写 pthread_mutex_t(),无需依赖下文讨论的扩展方法。本节内容保留仅供历史参考,但已不再适用于当前语言版本。
pthread 类型在 Swift 中使用起来相当麻烦。它们被定义为包含大量存储空间的不透明结构体(opaque struct),例如:
struct _opaque_pthread_mutex_t { long __sig; char __opaque[__PTHREAD_MUTEX_SIZE__]; };意图是先声明这些变量,然后用一个初始化函数来初始化它们,该函数接收一个指向存储区的指针并填充数据。用 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 initializedSwift 要求变量在使用前必须初始化。pthread_mutex_init 并不使用传入变量的现有值,它只是覆写该变量,但 Swift 编译器并不知道这一点,因此会报错。为了让编译器通过,需要用某个值来初始化这个变量,但这比看起来更困难。在类型名后面使用 () 并不起作用:
var mutex = pthread_mutex_t() // error: missing argument for parameter '__sig' in callSwift 要求为这些不透明字段提供值。__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) }要得到一个全零的大元组(tuple)并不容易,所以你必须全部手写出来:
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))这很糟糕,但我找不到更好的解决办法。我能想到的最好办法是将它封装在一个扩展中,这样空括号(empty ())就能正常工作。这是我创建的两个扩展:
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 的函数重载(function overloading)允许对所有不同类型使用相同的函数名。其基本形式如下:
func with(lock: SomeLockType, f: Void -> Void) { ...接着,在持有锁的情况下执行 f。让我们为所有这些类型实现它。
对于值类型,它需要一个指向锁的指针,这样上锁和解锁函数才能修改它。pthread_mutex_t 的实现就是调用相应的上锁和解锁函数,在它们之间调用 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 的函数式特性(functional nature),简单地写成:
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() }嗯,用户给了一段英文技术文本需要翻译成中文。这段内容是关于 Objective-C 运行时中 OSSpinLock 实现的描述。
根据用户提供的规则,需要完整翻译技术内容,保持术语一致性。这里 OSSpinLock 是自旋锁的技术术语,可以保留英文并加中文注释。
注意到这是连续段落中的一部分,用户之前已经发过类似结构的 Runtime 实现描述。需要保持翻译风格一致,技术术语处理方式也要相同。
想到要确保翻译准确,特别是” more of the same” 这个表达要译出” 同样模式” 的意思,同时符合技术文档的简洁风格。不需要添加额外解释,直接对应翻译即可。
不需要特别标注过时 API,因为自旋锁在现代系统中可能已较少使用,但原文没有明确指出版
func with(spinlock: UnsafeMutablePointer<OSSpinLock>, f: Void -> Void) { OSSpinLockLock(spinlock) f() OSSpinLockUnlock(spinlock) }模仿 @synchronized
有了这些封装,模仿 @synchronized 的基本功能相当简单。只需在你的类中添加一个属性来持有一个锁(lock),然后在之前使用 @synchronized 的地方改用 with:
let queue = dispatch_queue_create("com.example.myqueue", nil)
func setEntryForKey(key: Key, entry: Entry) { with(queue) { entries[key] = entry } }从闭包(block)中获取数据稍微麻烦一些。虽然 @synchronized 允许在块内直接返回,但 with 语句则不行。相反,你必须使用一个变量(var)并在闭包内部为其赋值:
func entryForKey(key: Key) -> Entry? { var result: Entry? with(queue) { result = entries[key] } return result }将样板代码封装成一个通用函数应该是可行的,但我目前在让 Swift 编译器的类型推断配合这一点上遇到了困难,暂时还没有解决方案。
模拟原子属性
Atomic properties(原子属性)并不常用。问题在于,与代码的其他许多有用属性不同,atomicity(原子性)不具备可组合性。例如,如果函数 f 不会内存泄漏,函数 g 也不会内存泄漏,那么仅调用 f 和 g 的函数 h 同样不会内存泄漏。原子性则不然。举个例子,假设你有一组原子的、线程安全的 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 中实现这一点,你需要一个用于执行锁操作的计算属性(computed property),以及一个真正存储值的常规属性(normal property):
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(POSIX 线程锁接口)可以立即排除,原因在于从 Swift 中使用它们比较困难,而且其他 API 同样能实现其功能。在 C 和 Objective-C 中我常喜欢用 pthread,因为它们相当直接且高效,但在此除非有特殊需求,否则不值得使用。
读写锁(reader / writer locks)大多不值得考虑。在常见场景中 —— 当读写操作都很快时,读写锁带来的额外开销反而超过了允许多读者并发的优势。
递归锁(recursive locks)很大程度上是引发死锁的诱因。它们在某些情况下有用,但如果你发现自己设计的方案中需要获取当前线程已持有的锁,这通常意味着你应该重新设计以避免这种需求。
我的看法是,如有疑问,默认使用 dispatch_queue_t(调度队列)。它们虽然更重量级,但这一点很少成为问题。其 API 相当便利,并且能确保你永远不会忘记将加锁调用与解锁调用配对。它们还提供了许多实用功能,例如:仅用单次 dispatch_async 调用就能在后台运行加锁代码;或直接针对该队列设置定时器等事件源,使其自动以加锁方式执行。你甚至可以通过 NSOperationQueue 的 underlyingQueue 属性,将其作为 NSNotificationCenter 观察者和 NSURLSession 代理等的目标,该属性是 OS X 10.10 和 iOS 8 新增的。(译注:underlyingQueue 属性现代系统仍支持)
NSOperationQueue 真希望能像 dispatch_queue_t 那样酷,但几乎没有理由将其用作锁的 API。它使用起来更繁琐,作为典型的锁 API 也没有提供任何优势,不过其操作的自动依赖管理功能在其他上下文中有时会很有用。
NSLock 是一个简单易用的锁定类,性能也相当不错。如果你出于某种原因需要显式的锁定和解锁调用,而不是使用 dispatch_queue_t 基于代码块的 API,它是个不错的选择。但在大多数情况下,几乎没有理由使用它。
OSSpinLock(自旋锁)非常适合用在锁的获取频繁、竞争不激烈且锁定代码执行迅速的场景中。它的开销要小得多,这有助于提升热代码路径的性能。相反,如果代码可能长时间持有锁,或者竞争很常见,它就不是一个好的选择,因为这会浪费 CPU 时间。总的来说,默认选择 dispatch_queue_t,但如果它开始出现在性能分析器中,可以考虑将 OSSpinLock 作为一种相当简单的优化手段。
结论 Swift 没有内置的线程同步语言设施,但这一缺陷被苹果框架中丰富的锁定 API 所弥补。GCD 和 dispatch_queue_t 仍然是杰作,该 API 在 Swift 中运行良好。我们没有 @synchronized(同步锁)或 atomic properties(原子属性),但我们有比它们更好的东西。
今日内容到此为止。下次再会时,将带来更多精彩的冒险故事。周五问答专栏的选题灵感源自各位读者的建议 —— 若有希望在此探讨的话题,请不吝投稿!
Original (English)
Source: https://www.mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html
An interesting aspect of Swift is that there’s nothing in the language related to threading, and more specifically to mutexes/locks. Even Objective-C has @synchronized and atomic properties. Fortunately, most of the power is in the platform APIs which are easily used from Swift. Today I’m going to explore the use of those APIs and the transition from Objective-C, a topic suggested by Cameron Pulsford.
A Quick Recap on LocksA lock, or mutex, is a construct that ensures only one thread is active in a given region of code at any time. They’re typically used to ensure that multiple threads accessing a mutable data structure all see a consistent view of it. There are several kinds of locks:
-
Blocking locks sleep a thread while it waits for another thread to release the lock. This is the usual behavior.
-
Spinlocks use a busy loop to constantly check to see if a lock has been released. This is more efficient if waiting is rare, but wastes CPU time if waiting is common.
-
Reader/writer locks allow multiple “reader” threads to enter a region simultaneously, but exclude all other threads (including readers) when a “writer” thread acquires the lock. This can be useful as many data structures are safe to read from multiple threads simultaneously, but unsafe to write while other threads are either reading or writing.
-
Recursive locks allow a single thread to acquire the same lock multiple times. Non-recursive locks can deadlock, crash, or otherwise misbehave when re-entered from the same thread.
APIsApple’s APIs have a bunch of different mutex facilities. This is a long but not exhaustive list:
-
pthread_mutex_t.
-
pthread_rwlock_t.
-
dispatch_queue_t.
-
NSOperationQueue when configured to be serial.
-
NSLock.
-
OSSpinLock.
In addition to this, Objective-C provides the @synchronized language construct, which at the moment is implemented on top of pthread_mutex_t. Unlike the others, @synchronized doesn’t use an explicit lock object, but rather treats an arbitrary Objective-C object as if it were a lock. A @synchronized(someObject) section will block access to any other @synchronized sections that use the same object pointer. These different facilities all have different behaviors and capabilities:
-
pthread_mutex_t is a blocking lock that can optionally be configured as a recursive lock.
-
pthread_rwlock_t is a blocking reader/writer lock.
-
dispatch_queue_t can be used as a blocking lock. It can be used as a reader/writer lock by configuring it as a concurrent queue and using barrier blocks. It also supports asynchronous execution of the locked region.
-
NSOperationQueue can be used as a blocking lock. Like dispatch_queue_t it supports asynchronous execution of the locked region.
-
NSLock is blocking lock as an Objective-C class. Its companion class NSRecursiveLock is a recursive lock, as the name indicates.
-
OSSpinLock is a spinlock, as the name indicates.
Finally, @synchronized is a blocking recursive lock.
Value TypesNote that pthread_mutex_t, pthread_rwlock_t, and OSSpinLock are value types, not reference types. That means that if you use = on them, you make a copy. This is important, because these types can’t be copied! If you copy one of the pthread types, the copy will be unusable and may crash when you try to use it. The pthread functions that work with these types assume that the values are at the same memory addresses as where they were initialized, and putting them somewhere else afterwards is a bad idea. OSSpinLock won’t crash, but you get a completely separate lock out of it which is never what you want.
If you use these types, you must be careful never to copy them, whether explicitly with a = operator, or implicitly by, for example, embedding them in a struct or capturing them in a closure.
Additionally, since locks are inherently mutable objects, this means you need to declare them with var instead of let.
The others are reference types, meaning they can be passed around at will, and can be declared with let.
InitializationUpdate 2015-02-10: the problems described in this section have been made obsolete with breathtaking speed. Apple released Xcode 6.3b1 yesterday which includes Swift 1.2. Among other changes, C structs are now imported with an empty initializer that sets all fields to zero. In short, you can now write pthread_mutex_t() without the extensions I discuss below. This section will remain for historical interest, but is no longer relevant to the language.
The pthread types are troublesome to use from Swift. They’re defined as opaque structs with a bunch of storage, such as:
struct _opaque_pthread_mutex_t { long __sig; char __opaque[__PTHREAD_MUTEX_SIZE__]; };The intent is that you declare them, then initialize them using an init function that takes a pointer to the storage and fills it out. In C, it looks like:
pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL);This works fine, as long as you remember to call pthread_mutex_init. However, Swift really, really dislikes uninitialized variables. The equivalent Swift fails to compile:
var mutex: pthread_mutex_t pthread_mutex_init(&mutex, nil) // error: address of variable 'mutex' taken before it is initializedSwift requires variables to be initialized before they’re used. pthread_mutex_init doesn’t use the value of the variable passed in, it just overwrites it, but Swift doesn’t know that and so it produces an error. To satisfy the compiler, the variable needs to be initialized with something, but that’s harder than it looks. Using () after the type doesn’t work:
var mutex = pthread_mutex_t() // error: missing argument for parameter '__sig' in callSwift requires values for those opaque fields. __sig is easy, we can just pass zero. __opaque is a bit more annoying. Here’s how it gets bridged into 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) }There’s no easy way to get a big tuple full of zeroes, so you have to write it all out:
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))This is awful, but I couldn’t find a good way around it. The best I could do was wrap it up in an extension so that the empty () works. Here are the two extensions I made:
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) } }With these extensions, this works:
var mutex = pthread_mutex_t() pthread_mutex_init(&mutex, nil)It may be possible to roll the call to pthread_mutex_init into the extension initializer as well, but there’s no guarantee that self in a struct init points to the variable being initialized. Since these values can’t be moved in memory after being initialized, I wanted to keep the initialization as a separate call.
Locking WrappersTo make these different APIs easier to use, I wrote a series of small wrapper functions. I settled on with as a convenient, short, syntax-looking name inspired by Python’s with statement. Swift’s function overloading allows using the same name for all these different types. The basic form looks like this:
func with(lock: SomeLockType, f: Void -> Void) { ...This then executes f with the lock held. Let’s implement it for all these types.
For the value types, it needs to take a pointer to the lock so the lock/unlock functions can modify it. The implementation for pthread_mutex_t just calls the appropriate lock and unlock functions, with a call to f in between:
func with(mutex: UnsafeMutablePointer<pthread_mutex_t>, f: Void -> Void) { pthread_mutex_lock(mutex) f() pthread_mutex_unlock(mutex) }The implementation for pthread_rwlock_t is almost identical:
func with(rwlock: UnsafeMutablePointer<pthread_rwlock_t>, f: Void -> Void) { pthread_rwlock_rdlock(rwlock) f() pthread_rwlock_unlock(rwlock) }I made a companion to this one that takes a write lock, which again looks much the same:
func with_write(rwlock: UnsafeMutablePointer<pthread_rwlock_t>, f: Void -> Void) { pthread_rwlock_wrlock(rwlock) f() pthread_rwlock_unlock(rwlock) }The one for dispatch_queue_t is even simpler. It’s just a wrapper around dispatch_sync:
func with(queue: dispatch_queue_t, f: Void -> Void) { dispatch_sync(queue, f) }In fact, if one were too clever for one’s own good and wanted to confuse people, one could take advantage of the functional nature of Swift and simply write:
let with = dispatch_syncThis is unwise for a couple of reasons, not the least of which being that it messes with the type-based overloading we’re trying to use here.
NSOperationQueue is conceptually similar, but there’s no direct equivalent to dispatch_sync. Instead, we create an operation, add it to the queue, and explicitly wait for it to finish:
func with(opQ: NSOperationQueue, f: Void -> Void) { let op = NSBlockOperation(f) opQ.addOperation(op) op.waitUntilFinished() }The implementation for NSLock looks like the pthread versions, just with slightly different locking calls:
func with(lock: NSLock, f: Void -> Void) { lock.lock() f() lock.unlock() }Finally, the OSSpinLock implementation is again more of the same:
func with(spinlock: UnsafeMutablePointer<OSSpinLock>, f: Void -> Void) { OSSpinLockLock(spinlock) f() OSSpinLockUnlock(spinlock) }Imitating @synchronizedWith these wrappers, imitating the basics of @synchronized is fairly simple. Add a property to your class that holds a lock, then use with where you would have used @synchronized before:
let queue = dispatch_queue_create("com.example.myqueue", nil)
func setEntryForKey(key: Key, entry: Entry) { with(queue) { entries[key] = entry } }Getting data out of the block is a bit less pleasant, unfortunately. While @synchronized lets you return from within, that doesn’t work with with. Instead, you have to use a var and assign to it within the block:
func entryForKey(key: Key) -> Entry? { var result: Entry? with(queue) { result = entries[key] } return result }It should be possible to wrap the boilerplate in a generic function, but I had trouble getting the Swift compiler’s type inference to play along and don’t have a solution just yet.
Imitating Atomic PropertiesAtomic properties are not often useful. The problem is that, unlike many other useful properties of code, atomicity doesn’t compose. For example, if function f doesn’t leak memory, and function g doesn’t leak memory, then function h that just calls f and g also doesn’t leak memory. The same is not true of atomicity. For an example, imagine you have a set of atomic, thread-safe Account classes:
let checkingAccount = Account(amount: 100) let savingsAccount = Account(amount: 0)Now you move the money to savings:
checkingAccount.withDraw(100) savingsAccount.deposit(100)In another thread, you total up the balance and tell the user:
println("Your total balance is: \(checkingAccount.amount + savingsAccount.amount)")If this runs at just the wrong time, it will print zero instead of 100, despite the fact that the Account objects themselves are fully atomic and the user had a balance of 100 the whole time. Because of this, it’s usually better to build entire subsystems to be atomic, rather than individual properties.
There are rare cases where atomic properties are useful, because it really is a standalone thing that just needs to be thread safe. To achieve that in Swift, you need a computed property that will do the locking, and a second normal property that will actually hold the value:
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 } } }Choosing Your Lock APIThe pthread APIs can be discounted immediately due to the difficulty of using them from Swift, and the fact that they don’t do anything that other APIs don’t also do. I often like to use them in C and Objective-C because they’re fairly straightforward and fast, but it’s not worth it here unless something really requires it.
Reader/writer locks are mostly not worth worrying about. For common cases, where reads and writes are quick, the extra overhead used by a reader/writer lock outweighs the ability to have multiple concurrent readers.
Recursive locks are mostly an invitation to deadlock. There are cases where they’re useful, but if you find yourself with a design where you need to take a lock that’s already locked on the current thread, that’s a good sign you should probably rethink it so that’s not necessary.
My opinion is that, when in doubt, default to dispatch_queue_t. They’re more heavyweight, but this rarely matters. The API is reasonably convenient, and they ensure you never forget to pair a lock call with an unlock call. They provide a ton of nice facilities that can come in handy, like the ability to use a single dispatch_async call to run locked code in the background, or the ability to set up timers or other event sources targeted directly at the queue so that they automatically execute locked. You can even use it as a target for things like NSNotificationCenter observers and NSURLSession delegates by using the underlyingQueue property of NSOperationQueue, new in OS X 10.10 and iOS 8.
NSOperationQueue wishes it could be as cool as dispatch_queue_t and there are few if any reasons to use it as a lock API. It’s more cumbersome to use and doesn’t provide any advantages for typical use as a locking API, although the automatic dependency management for operations can sometimes be useful in other contexts.
NSLock is a simple locking class that’s easy to use and reasonably fast. It’s a good choice if you want explicit lock and unlock calls for some reason, rather than the blocks-based API of dispatch_queue_t, but there’s little reason to use it in most cases.
OSSpinLock is an excellent choice for uses where the lock is taken often, contention is low, and the locked code runs quickly. It has much lower overhead and this helps performance for hot code paths. On the other hand, it’s a bad choice for uses where code may hold the lock for a substantial amount of time, or contention is common, as it will waste CPU time. In general, default to dispatch_queue_t, but keep OSSpinLock in mind as a fairly easy optimization if it starts to show up in the profiler.
ConclusionSwift has no language facilities for thread synchronization, but this deficiency is more than made up for the wealth of locking APIs available in Apple’s frameworks. GCD and dispatch_queue_t are still a masterpiece and the API works great in Swift. We don’t have @synchronized or atomic properties, but we have things that are better.
That’s it for today. Come back next time for more exciting adventures. Friday Q&A is built on the topic suggestions of readers like you, so if you have something you’d like to see covered here, please send it in!