Deadlock in Singleton | Swift | iOS

Igor Sorokin (srk1nn)
2 min readJan 30, 2024

--

In this post, I want to share my experience of how I was involved in fixing bugs related to GCD and Singleton.

deadlocks swift

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 calls DispatchQueue.main.sync, so it must wait until the main thread sets UIApplication.State to state 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.

--

--

Igor Sorokin (srk1nn)
Igor Sorokin (srk1nn)

No responses yet