class SomeViewController: UIViewController {
let semaphore = DispatchSemaphore(value: 1)
deinit {
semaphore.signal() // just in case?
}
func someLongAsyncTask() {
semaphore.wait()
...
semaphore.signal() // called much later
}
}
If I tell a semaphore to wait and then deinitialize the view controller that owns it before the semaphore was ever told to signal, the app crashes with an Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
error. However, if I simply call semaphore.signal()
in the deinit
method of the view controller, disaster averted. 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?
You have stumbled into a feature/bug in DispatchSemaphore
. If you look at the stack trace and jump to the top of the stack, you'll see assembly with a message:
BUG IN CLIENT OF LIBDISPATCH: Semaphore object deallocated while in use
E.g.,
This is because DispatchSemaphore
checks to see whether the semaphore’s associated value
is less at deinit
than at init
, and if so, it fails. In short, if the value is less, libDispatch concludes that the semaphore is still being used.
This may appear to be overly conservative, as this generally happens if the client was sloppy, not because there is necessarily some serious problem. And it would be nice if it issued a meaningful exception message rather forcing us to dig through stack traces. But that is how libDispatch works, and we have to live with it.
All of that having been said, there are three possible solutions:
You obviously have a path of execution where you are wait
ing and not reaching the signal
before the object is being deallocated. Change the code so that this cannot happen and your problem goes away.
While you should just make sure that wait
and signal
calls are balanced (fixing the source of the problem), you can use the approach in your question (to address the symptoms of the problem). But that deinit
approach solves the problem through the use of non-local reasoning. If you change the initialization, so the value is, for example, five, you or some future programmer have to remember to also go to deinit
and insert four more signal
calls.
The other approach is to instantiate the semaphore with a value of zero and then, during initialization, just signal
enough times to get the value up to where you want it. Then you won’t have this problem. This keeps the resolution of the problem localized in initialization rather than trying to have to remember to adjust deinit
every time you change the non-zero value during initialization.
See https://lists.apple.com/archives/cocoa-dev/2014/Apr/msg00483.html.
Itai enumerated a number of reasons that one should not use semaphores at all. There are lots of other reasons, too:
Nowadays, semaphores are almost always the wrong solution. If you tell us what problem you are trying to solve with the semaphore, we might be able to recommend other, better, solutions.
You said:
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?
Technically speaking, over-signaling does not introduce new problems, so you don't really have to worry about that. But this “just in case” over-signaling does have a hint of code smell about it. It tells you that you have cases where you are waiting but never reaching signaling, which suggests a logic error (see point 1 above).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With