Mobile Application Threads and Concurrency Optimization
Deadlock in iOS application reproduces unstably: once every 20–30 minutes application hangs completely. Crash logs contain nothing, because this is not a crash, it's a deadlock. Thread state dump through Xcode shows: main thread blocked on DispatchQueue.sync to SerialQueue, and SerialQueue waits for completion handler trying to execute on main thread. Classic two-thread deadlock.
Concurrency — one of most complex topics in mobile development. Data races, deadlocks, UI updates from wrong thread — these errors appear rarely, reproduce unstably and cost dear in production.
Typical Thread Problems
UI Updates from Wrong Thread
On Android: CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. Cause — processing network response directly in Retrofit callback without withContext(Dispatchers.Main).
On iOS: Main Thread Checker in Xcode (enabled by default in Scheme settings) catches UIKit access from background threads in debug builds. In release — random crashes or visual corruption.
Correct iOS pattern:
DispatchQueue.global(qos: .userInitiated).async {
let result = heavyComputation()
DispatchQueue.main.async {
self.label.text = result // only here
}
}
Thread Explosion with GCD
DispatchQueue.global().async creates new thread on each call if all worker threads busy. At 64+ simultaneous async tasks, system starts creating threads aggressively — this is thread explosion. Symptom: everything works fine, then sharp performance degradation under load.
Solution: limited concurrency through OperationQueue.maxConcurrentOperationCount or through Swift Concurrency TaskGroup with explicit withTaskGroup and limited parallelism:
await withTaskGroup(of: Result.self) { group in
for item in items.prefix(4) { // no more than 4 parallel tasks
group.addTask { await process(item) }
}
}
Data Races
Multiple threads read and write same field without synchronization. On Swift — Thread Sanitizer (TSan) finds data races in debug builds. Enabled in Scheme → Diagnostics → Thread Sanitizer.
Synchronization options:
-
NSLock/os_unfair_lock— fast mutexes for critical sections -
DispatchQueue(label:attributes:.concurrent)withbarrierfor read-write lock pattern -
actorin Swift 5.5+ — most modern way, compiler guarantees data isolation
actor UserCache {
private var storage: [String: User] = [:]
func get(_ id: String) -> User? { storage[id] }
func set(_ user: User) { storage[user.id] = user }
}
With actor compiler won't allow accessing storage outside actor-context without await.
Android: Incorrect Coroutines Usage
GlobalScope.launch — red flag. Coroutine lives infinitely, doesn't cancel when screen closes. On re-open — second one created. Correct — viewModelScope.launch (cancels on onCleared) or lifecycleScope.launch (cancels on onDestroy).
Dispatchers.Main vs Dispatchers.Main.immediate: when called from main thread, Dispatchers.Main.immediate executes synchronously without context switch — important for animations and immediate UI updates.
Improper exception handling in coroutines:
// WRONG — exception won't be caught
scope.launch {
try { riskyOperation() } catch (e: Exception) { handle(e) }
}
// CORRECT — CoroutineExceptionHandler for structured handling
val handler = CoroutineExceptionHandler { _, e -> handleError(e) }
scope.launch(handler) { riskyOperation() }
Diagnostics Tools
| Tool | Platform | What It Finds |
|---|---|---|
| Thread Sanitizer (TSan) | iOS / Android | Data races |
| Main Thread Checker | iOS | UI from background thread |
| Instruments → Time Profiler | iOS | Blocked threads |
| Android Studio Profiler → Threads | Android | Thread states, sleep/block/run |
| StrictMode | Android | Disk/network on main thread |
| Kotlin Coroutines Debugger | Android | Active coroutines, their stacks |
StrictMode on Android — enable in debug build:
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads().detectNetworkOnMainThread()
.penaltyLog().penaltyFlashScreen()
.build()
)
Screen flashing on violation — impossible to ignore.
Case: Deadlock in Swift
E-commerce application: adding to cart UI sometimes froze for 30–60 seconds. Reproduced only on poor internet.
Through Thread State dump found: CartService.addItem() called userDefaults.synchronize() inside serialQueue.sync, and synchronize() inside waited NSFileCoordinator, also standing in write queue. With network delay, several addItem() calls lined up and one fell into deadlock with NSFileCoordinator.
Solution: removed synchronize() (no-op in iOS 12+), moved cart saving to async write through DispatchQueue.global().async.
Work Stages
- Enable TSan and Main Thread Checker on all test runs
- Analyze Thread state in Instruments / Android Profiler Threads view
- Check all places with
synccalls and shared mutable state - Fix: weak references, correct dispatch queues, actor isolation
- Load testing to reveal race conditions under stress
Timeframe
Concurrency audit — 2–4 days. Fixing found issues — 3–14 days depending on depth of architectural decisions.







