以防万一,在取消初始化之前发出信号量是安全的吗?
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
检查信号量的关联 value
在 deinit
处是否小于 init
,如果是,则失败。简而言之,如果该值较小,则 libDispatch 断定该信号量仍在使用中。
这可能看起来过于保守,因为如果客户马虎,这通常会发生,而不是因为必然存在一些严重的问题。如果它发出有意义的异常消息而不是强迫我们挖掘堆栈跟踪,那就太好了。但这就是 libDispatch 的工作方式,我们必须忍受它。
综上所述,存在三种可能的解决方案:
你显然有一个执行路径,你在 wait
ing 而不是在对象被释放之前到达 signal
。更改代码,这样就不会发生这种情况,您的问题就会消失。
虽然您应该确保 wait
和 signal
调用是平衡的(修复问题的根源),但您可以在问题中使用该方法(以解决问题的症状)。但是 deinit
方法通过使用非局部推理解决了问题。如果您更改初始化,那么值是,例如,5,您或未来的某些程序员必须记住也转到 deinit
并插入另外四个 signal
调用。
另一种方法是用零值实例化信号量,然后在初始化过程中,只需 signal
足够的次数就可以将值设置到您想要的位置。那么你就不会有这个问题。这使得问题的解决方案在初始化过程中局部化,而不是每次在初始化过程中更改非零值时都必须记住调整 deinit
。
参见 https://lists.apple.com/archives/cocoa-dev/2014/Apr/msg00483.html。
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 点)。
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
检查信号量的关联 value
在 deinit
处是否小于 init
,如果是,则失败。简而言之,如果该值较小,则 libDispatch 断定该信号量仍在使用中。
这可能看起来过于保守,因为如果客户马虎,这通常会发生,而不是因为必然存在一些严重的问题。如果它发出有意义的异常消息而不是强迫我们挖掘堆栈跟踪,那就太好了。但这就是 libDispatch 的工作方式,我们必须忍受它。
综上所述,存在三种可能的解决方案:
你显然有一个执行路径,你在
wait
ing 而不是在对象被释放之前到达signal
。更改代码,这样就不会发生这种情况,您的问题就会消失。虽然您应该确保
wait
和signal
调用是平衡的(修复问题的根源),但您可以在问题中使用该方法(以解决问题的症状)。但是deinit
方法通过使用非局部推理解决了问题。如果您更改初始化,那么值是,例如,5,您或未来的某些程序员必须记住也转到deinit
并插入另外四个signal
调用。另一种方法是用零值实例化信号量,然后在初始化过程中,只需
signal
足够的次数就可以将值设置到您想要的位置。那么你就不会有这个问题。这使得问题的解决方案在初始化过程中局部化,而不是每次在初始化过程中更改非零值时都必须记住调整deinit
。参见 https://lists.apple.com/archives/cocoa-dev/2014/Apr/msg00483.html。
Itai 列举了一些根本不应该使用信号量的原因。还有很多其他原因:
- 信号量与新的 Swift 并发系统不兼容(参见 Swift concurrency: Behind the scenes);
- 如果在代码中不精确,信号量也很容易引入死锁;
- 信号量通常与可取消的异步例程相对立;等等
如今,信号量几乎总是错误的解决方案。如果您告诉我们您试图用信号量解决什么问题,我们也许可以推荐其他更好的解决方案。
你说:
However, if the async function returns before
deinit
is called and the view controller is deinitialized, thensignal()
is called twice, which doesn't seem problematic. But is it safe and/or wise to do this?
从技术上讲,过度发送信号不会引入新问题,所以您真的不必为此担心。但是这种“以防万一”的过度信号确实带有一丝代码味道。它告诉您您遇到了正在等待但从未到达信号的情况,这表明存在逻辑错误(请参见上面的第 1 点)。