Deadlock in Singleton | Swift | iOS
In this post, I want to share my experience of how I was involved in fixing bugs related to GCD and Singleton.
Projects that support lower iOS 13 or have a legacy codebase, probably use GCD or Operation. I was involved in fixing bugs related to concurrency (especially with GCD) and Singleton. So now, I want to share my experience.
Problem
To understand a problem, let’s talk about serial queues and sync methods. I want to remember that deadlocks can happen, when a serial queue sync operation triggers another sync operation on the same queue.
In our project, we encountered this when using the Singleton pattern. We have an AppStateProvider
, which can be used from any thread, but initialization must be on the main thread (applicationState
requires this). This provider creates during Manager creation. The simplified version is listed below.
final class AppStateProvider {
private(set) var state: UIApplication.State
init() {
if Thread.isMainThread {
state = UIApplication.shared.applicationState
} else {
DispatchQueue.main.sync {
state = UIApplication.shared.applicationState
}
}
}
}
final class Manager {
static let shared: Manager = {
let provider = AppStateProvider()
return Manager(provider: provider)
}()
private let provider: AppStateProvider
init(provider: AppStateProvider) {
self.provider = provider
}
// other code
}
Remember, that static let are guaranteed to be initialized only once, even when accessed by multiple threads simultaneously. So static let is tread-safe.
Everything looks good. But this code may cause a deadlock.
To understand this let’s talk about how static let guarantees to be initialized once. Internally it uses dispatch_once. Who is not familiar with Objective-C, it acts like NSLock. It locks a thread until initialization finishes.
So putting it together, imagine the situation:
- a background thread calls
Manager.shared
and starts the Singleton creation process - the main thread also calls
Manager.shared
and waits until a background thread finishes creation - a background thread calls
AppStateProvider
initializer. As a result, it callsDispatchQueue.main.sync
, so it must wait until the main thread setsUIApplication.State
tostate
property - The main thread waits for the
static let
initialization, the background thread waits for the main thread. Deadlock...
Final thoughts
There is no advice how to avoid this for all cases. Even if we remove DispatchQueue.main.sync
from the initializer and add it to the property, we don't resolve our problem. Because we can create a deadlock, which I describe at the start of the Problem section.
In my opinion general rule here, is to use async with serial queues. And design your API with that in mind.