Deadlock in Singleton | Swift
Introduction
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
General rule here, is to use