以防万一,在取消初始化之前发出信号量是安全的吗?

Safe to signal semaphore before deinitialization just in case?

class SomeViewController: UIViewController {
    let semaphore = DispatchSemaphore(value: 1)

    deinit {
        semaphore.signal() // just in case?
    }

    func someLongAsyncTask() {
        semaphore.wait()
        ...
        semaphore.signal() // called much later
    }
}

如果我告诉信号量等待,然后在信号量被告知发出信号之前取消初始化拥有它的视图控制器,应用程序会崩溃并出现 Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) 错误。但是,如果我简单地在视图控制器的 deinit 方法中调用 semaphore.signal(),就可以避免灾难。但是,如果deinit之前的异步函数returns被调用,视图控制器被去初始化,那么signal()被调用了两次,这看起来没有问题。但是这样做安全 and/or 明智吗?

您在 DispatchSemaphore 中遇到了 feature/bug。如果您查看堆栈跟踪并跳转到堆栈顶部,您将看到带有消息的程序集:

BUG IN CLIENT OF LIBDISPATCH: Semaphore object deallocated while in use

例如,

这是因为 DispatchSemaphore 检查信号量的关联 valuedeinit 处是否小于 init,如果是,则失败。简而言之,如果该值较小,则 libDispatch 断定该信号量仍在使用中。

这可能看起来过于保守,因为如果客户马虎,这通常会发生,而不是因为必然存在一些严重的问题。如果它发出有意义的异常消息而不是强迫我们挖掘堆栈跟踪,那就太好了。但这就是 libDispatch 的工作方式,我们必须忍受它。

综上所述,存在三种可能的解决方案:

  1. 你显然有一个执行路径,你在 waiting 而不是在对象被释放之前到达 signal。更改代码,这样就不会发生这种情况,您的问题就会消失。

  2. 虽然您应该确保 waitsignal 调用是平衡的(修复问题的根源),但您可以在问题中使用该方法(以解决问题的症状)。但是 deinit 方法通过使用非局部推理解决了问题。如果您更改初始化,那么值是,例如,5,您或未来的某些程序员必须记住也转到 deinit 并插入另外四个 signal 调用。

    另一种方法是用零值实例化信号量,然后在初始化过程中,只需 signal 足够的次数就可以将值设置到您想要的位置。那么你就不会有这个问题。这使得问题的解决方案在初始化过程中局部化,而不是每次在初始化过程中更改非零值时都必须记住调整 deinit

    参见 https://lists.apple.com/archives/cocoa-dev/2014/Apr/msg00483.html

  3. Itai 列举了一些根本不应该使用信号量的原因。还有很多其他原因:

    • 信号量与新的 Swift 并发系统不兼容(参见 Swift concurrency: Behind the scenes);
    • 如果在代码中不精确,信号量也很容易引入死锁;
    • 信号量通常与可取消的异步例程相对立;等等

    如今,信号量几乎总是错误的解决方案。如果您告诉我们您试图用信号量解决什么问题,我们也许可以推荐其他更好的解决方案。


你说:

However, if the async function returns before deinit is called and the view controller is deinitialized, then signal() is called twice, which doesn't seem problematic. But is it safe and/or wise to do this?

从技术上讲,过度发送信号不会引入新问题,所以您真的不必为此担心。但是这种“以防万一”的过度信号确实带有一丝代码味道。它告诉您您遇到了正在等待但从未到达信号的情况,这表明存在逻辑错误(请参见上面的第 1 点)。