Async Startup and Locking
I try to avoid the following pattern, but it’s the best call from time to time.
Foo does some startup in a background serial queue. Parts of Foo’s public API don’t work until that startup has completed, so I want those methods to block callers until startup has completed.
I have not found a great way of dealing with this.
I do something like this:
- init create_lock() lock() startupOnSerialQueue() completion: unlock() - someMethod lock() do_the_thing() unlock()
With this method,
someMethod is blocked while
startupOnSerialQueue() is happening. This is good.
someMethod also gets a lock all the rest of the time. That’s not a big deal, because, in practice,
someMethod is super-fast. But it wouldn’t actually have to have that lock except right at startup.
This feels inelegant to me. It’s simple enough, and it works, but I don’t like it.
Do you know of a better solution?
My first thought is that
dispatch_once would help — but it wouldn’t. (Not that I can see.) It would make sure
startupOnSerialQueue is called once, but doesn’t solve the problem of blocking use of
startupOnSerialQueue has completed.
someMethod could put
do_the_thing() inside a
dispatch_sync block that runs on the background serial queue. But that also seems inelegant. Needlessly complicated.
This reminds me of the old joke. Man: “Doc, it hurts when I go like this!” Doctor: “Well don’t go like that!”
Except that I will go like that when everything else about the design is perfect. Since I can keep the async startup internal to Foo, it’s able to implement its public API without infecting anything else.
So my only question is if there’s a better pattern for handling this.
Update 4:30 pm: To be clear: I believe in serial queues. Absolutely love ’em.
I’ve had varied feedback on Twitter and email.
In the end, this is what I’m doing.
Using an NSLock property — self.startupLock — instead of a pthread_mutex_t. (I always forget about NSLock. But it’s easy and cleaner-looking than pthread mutexes.)
Not just unlocking the lock upon startup completion but nilling it out (as Justin Miller and others have suggested) since it’s no longer needed once unlocked. (In my specific case all access is main thread only, so this is fine.)
In the end this still isn’t beautiful, but it’s simpler and less code than everything else and it solves the problem of making sure that 1) startupOnSerialQueue() doesn’t block the main thread, and 2) someMethod blocks until startupOnSerialQueue() completes, and is fast the rest of the time.
The most interesting of the feedback suggested that this is a job for continuations and futures. So: something to learn. I’ll start with Mike Ash’s article on futures.
Update 10:30 am the next day: Here’s what I really ended up doing.
Note that the previous solution had a deadlock. Sheesh. It was dumb anyway.
Instead I’m just doing startupOnSerialQueue via dispatch_sync. No lock needed.
So it blocks the main thread, but it’s not noticeable. (It’s about as fast as reading a 5K binary plist from disk, which I hadn’t realized at first. That’s not what it’s doing, but performance is comparable.)
Were startupOnSerialQueue slower, I’d have to think of something else. But since it’s fast enough, it’s fine.
So it looks like this: