关于 Swift defer 的正确使用

其实这篇文章的缘起是由于在对 Kingfisher 做重构的时候,因为自己对 defer 的理解不够准确,导致了一个 bug。所以想藉由这篇文章探索一下 defer 这个关键字的一些 edge case。

典型用法

Swift 里的 defer 大家应该都很熟悉了,defer 所声明的 block 会在当前代码执行退出后被调用。正因为它提供了一种延时调用的方式,所以一般会被用来做资源释放或者销毁,这在某个函数有多个返回出口的时候特别有用。比如下面的通过 FileHandle 打开文件进行操作的方法:

func operateOnFile(descriptor: Int32) {
    let fileHandle = FileHandle(fileDescriptor: descriptor)

    let data = fileHandle.readDataToEndOfFile()

    if /* onlyRead */ {
        fileHandle.closeFile()
        return
    }

    let shouldWrite = /* 是否需要写文件 */
    guard shouldWrite else {
        fileHandle.closeFile()
        return
    }

    fileHandle.seekToEndOfFile()
    fileHandle.write(someData)
    fileHandle.closeFile()
}

我们在不同的地方都需要调用 fileHandle.closeFile() 来关闭文件,这里更好的做法是用 defer 来统一处理。这不仅可以让我们就近在资源申请的地方就声明释放,也减少了未来添加代码时忘记释放资源的可能性:

func operateOnFile(descriptor: Int32) {
    let fileHandle = FileHandle(fileDescriptor: descriptor)
    defer { fileHandle.closeFile() }
    let data = fileHandle.readDataToEndOfFile()

    if /* onlyRead */ { return }

    let shouldWrite = /* 是否需要写文件 */
    guard shouldWrite else { return }

    fileHandle.seekToEndOfFile()
    fileHandle.write(someData)
}

defer 的作用域

在做 Kingfisher 重构时,对线程安全的保证我选择使用了 NSLock 来完成。简单说,会有一些类似这样的方法:

let lock = NSLock()
let tasks: [ID: Task] = [:]

func remove(_ id: ID) {
    lock.lock()
    defer { lock.unlock() }
    tasks[id] = nil
}

对于 tasks 的操作可能发生在不同线程中,用 lock() 来获取锁,并保证当前线程独占,然后在操作完成后使用 unlock() 释放资源。这是很典型的 defer 的使用方式。

但是后来出现了一种情况,即调用 remove 方法之前,我们在同一线程的 caller 中获取过这个锁了,比如:

func doSomethingThenRemove() {
    lock.lock()
    defer { lock.unlock() }

    // 操作 `tasks`
    // ...

    // 最后,移除 `task`
    remove(123)
}

这样做显然在 remove 中造成了死锁 (deadlock):remove 里的 lock() 在等待 doSomethingThenRemove 中做 unlock() 操作,而这个 unlockremove 阻塞了,永远不可能达到。

解决的方法大概有三种:

  1. 换用 NSRecursiveLockNSRecursiveLock 可以在同一个线程获取多次,而不造成死锁的问题。
  2. 在调用 remove 之前先 unlock
  3. remove 传入按照条件,避免在其中加锁。

1 和 2 都会造成额外的性能损失,虽然在一般情况下这样的加锁性能微乎其微,但是使用方案 3 似乎也并不很麻烦。于是我很开心地把 remove 改成了这样:

```swift
func remove(_ id: ID, acquireLock: Bool) {
if acquireLock {
lock.lock()
defer { lock.unlock() }

top Created with Sketch.