Home > database >  Safe to signal semaphore before deinitialization just in case?
Safe to signal semaphore before deinitialization just in case?

Time:12-24

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?

CodePudding user response:

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.,

enter image description here

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:

  1. You obviously have a path of execution where you are waiting and not reaching the signal before the object is being deallocated. Change the code so that this cannot happen and your problem goes away.

  2. Your approach solves the problem, but requires non-local reasoning. If you change the initialization, so the value is, for example, five, you 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.

  3. Itai enumerated a number of reasons that one should not use semaphores at all. There are lots of other reasons, too (e.g., incompatible with new Swift concurrency system, can easily introduce deadlocks if not precise in one’s code, antithetical to cancellable asynchronous routines, etc.). 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, then signal() 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).

  • Related