8
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)错误而崩溃。但是,如果我只是调用视图控制器semaphore.signal()deinit方法,就可以避免灾难。但是,如果 async 函数在deinit被调用之前返回并且视图控制器被取消初始化,那么signal()就会被调用两次,这似乎没有问题。但是这样做是否安全和/或明智?

4

1 回答 1

4

您偶然发现了DispatchSemaphore. 如果您查看堆栈跟踪并跳转到堆栈顶部,您将看到带有消息的程序集:

LIBDISPATCH 客户端中的错误:信号量对象在使用时被释放

例如,

在此处输入图像描述

这是因为DispatchSemaphore检查信号量的关联value是否小于 at deinitinit如果是,则失败。简而言之,如果该值较小,则 libDispatch 会得出仍在使用信号量的结论。

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

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

  1. 您显然有一条执行路径,您正在执行并且在对象被释放之前wait没有到达。signal更改代码,这样就不会发生这种情况,您的问题就会消失。

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

    另一种方法是使用零值来实例化信号量,然后在初始化期间,只需signal足够的时间将值达到您想要的位置。那你就不会有这个问题了。这使问题的解决方法保持在初始化中,而不是deinit每次在初始化期间更改非零值时都必须记住进行调整。

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

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

    • 信号量与新的 Swift 并发系统不兼容(请参阅Swift 并发:幕后);
    • 如果在代码中不精确,信号量也很容易引入死锁;
    • 信号量通常与可取消的异步例程相反。等等

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


你说:

但是,如果 async 函数在deinit被调用之前返回并且视图控制器被取消初始化,那么signal()就会被调用两次,这似乎没有问题。但是这样做是否安全和/或明智?

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

于 2021-12-23T07:22:36.520 回答