Every mobile application performs many tasks simultaneously: loading data from the network, processing user touches, animating the interface and saving files. If all this code runs in a single thread, the application freezes with any network delay. Multithreading and concurrency are key concepts that allow the application to remain responsive and efficient. In this article we will cover all the main tools: from Main Thread and RunLoop to Kotlin coroutines and Combine on iOS. The material is based on Apple's official GCD documentation.
Key Takeaways
Multithreading is the ability of an application to execute several pieces of code simultaneously. Each piece runs in a separate thread — a lightweight process with its own call stack. In mobile development, threads are divided into two categories: Main Thread (UI thread) and Background Threads.
The operating system itself manages the distribution of threads across processor cores. Modern devices have 6–8 cores, so parallel execution can actually speed up work. However, creating threads is an expensive operation, so working directly with Thread is not recommended. Instead, higher-level abstractions are used: DispatchQueue, OperationQueue, CoroutineDispatcher.
Concurrency is a broader concept than multithreading. Concurrency means that tasks can be executed "simultaneously" even on a single core through context switching. Asynchrony (Async/Await) is a programming model where a task does not block a thread, but returns control while waiting for a result. Modern languages (Kotlin, Swift, Dart) have built-in Async/Await support.
At IT Sectr, we pay special attention to proper multithreading architecture at the start of a project. Mistakes made at an early stage lead to hard-to-catch bugs: data races, deadlocks, and application instability under load. Each of our projects undergoes a concurrency architecture review at the planning stage.
Main Thread — the only thread in a mobile application that has access to the UI. On Android it is called UI Thread, on iOS — Main Thread. All interface operations — changing text, animation, touch handling — are performed only on the Main Thread. If a heavy operation (file loading, JSON parsing) is performed on the main thread, the interface stops responding. On Android this leads to ANR (Application Not Responding), on iOS — to a "frozen" screen.
Background Threads are intended for everything not related to the UI: network requests, database operations, image processing, cryptography. After completion, the result is passed to the Main Thread for display. Each platform provides its own tools for switching between threads: DispatchQueue.main.async in iOS, runOnUiThread or withContext(Dispatchers.Main) in Android.
RunLoop — the event processing loop on the iOS main thread. RunLoop waits for events (touches, timers, notifications) and dispatches them to the appropriate handlers. On Android the equivalent is Looper, associated with each Main Thread. Main Looper infinitely extracts messages from the queue and passes them to Handler for processing. Understanding RunLoop and Looper helps avoid memory leaks and interface "stuttering".
Grand Central Dispatch (GCD) is an Apple library for managing multithreading at the C language level. GCD works with DispatchQueue — task queues. The developer does not create threads manually; GCD manages a thread pool, distributing tasks across available processor cores. DispatchQueues are of two types: Serial Queue (tasks execute one after another) and Concurrent Queue (tasks can execute simultaneously).
Main DispatchQueue is a serial queue bound to the main thread. Global Queues are concurrent queues with different priorities (QoS — Quality of Service): userInteractive, userInitiated, utility, background. Choosing the right QoS is critical for performance: .userInteractive — for tasks affecting the UI (animations, rendering); .background — for non-time-critical tasks (synchronization, cache cleanup).
OperationQueue is an abstraction over GCD with additional capabilities: task cancellation, setting dependencies between operations, controlling the maximum number of concurrent operations. Operations are objects of the Operation class (or BlockOperation). Example: if you need to load an image, then apply a filter, and only then display it — OperationQueue with dependencies handles it perfectly. In GCD you would have to manually synchronize these steps using DispatchGroup or semaphore.
Async/Await in Swift 5.5+ is a modern alternative to GCD. The keywords async and await make asynchronous code linear and readable. Functions are marked as async, and calls are awaited. The system itself manages context switching: by default, an async function runs on a background thread, while UI updates run on MainActor. @MainActor is an attribute that guarantees code execution on the main thread.
Coroutines are lightweight threads for Kotlin developed by JetBrains. Unlike regular threads, coroutines are not tied to a specific Thread. Thousands of coroutines can run on several threads without significant overhead. CoroutineScope manages the lifecycle of coroutines: viewModelScope is bound to ViewModel, lifecycleScope — to Activity/Fragment. When the scope is destroyed, all child coroutines are automatically cancelled.
Dispatchers determine which thread pool the coroutine runs on: Dispatchers.Main — UI thread; Dispatchers.IO — for network requests and disk operations; Dispatchers.Default — for CPU-intensive computations. To switch dispatchers, withContext is used. Coroutines support structured concurrency: each coroutine has a parent, and when the parent is cancelled, all child coroutines are cancelled. This prevents memory leaks and hanging tasks.
Flow — a cold asynchronous data stream from the coroutines library. Flow emits values sequentially: (1) the producer generates data, (2) operators transform the stream, (3) the collector consumes the result. Unlike LiveData, Flow supports complex operator chains (map, filter, flatMapConcat, catch) and is completely thread-safe. StateFlow and SharedFlow are hot variants of Flow, ideal for UI state and one-shot events (Snackbar, navigation).
Channel — another coroutine abstraction for passing data between coroutines. Channel works like a queue: one sender (send) and one or more receivers (receive). Buffered channels (Channel(UNLIMITED), Channel(BUFFERED)) allow configuring behavior on overflow. Channel is often used with Flow for bridging callback-based API into coroutines: callbackFlow { … }.
At IT Sectr we actively use coroutines and Flow in all Android projects. This allows writing asynchronous code that looks synchronous, is easy to test (runTest, TestDispatcher) and does not require manual thread management. Example of a simple coroutine with data loading:
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) {
return@withContext try {
val users = api.fetchUsers()
dao.insertAll(users)
users
} catch (e: Exception) {
dao.getAll()
}
}
}
Reactive programming is a paradigm where data propagates as asynchronous streams (Observable, Publisher). RxJava/RxKotlin is the most popular implementation for Android, ported from .NET Rx. RxSwift is a similar library for iOS. Main components: Observable (event source), Observer (subscriber), Scheduler (thread management), Operators (stream transformation).
Combine is an Apple framework for reactive programming introduced in iOS 13. Combine uses Publisher and Subscriber protocols. Unlike RxSwift, Combine is built into the SDK and closely integrated with SwiftUI. Combine operators: map, filter, combineLatest, zip, debounce, throttle — cover most scenarios from data binding to UI to search query debouncing.
Future and Promise — patterns for working with a single asynchronous result. Future represents a value that will be available later. Promise is a commitment to provide a value. In Rx this is Single (one successful response or error), in Combine — Future Publisher. In practice, Future/Promise are convenient for single API requests, while Observable/Publisher are for continuous streams (geolocation, text input).
Callback and Delegate — classic patterns for asynchronous operations. Callback — a function passed as an argument and called upon operation completion. Delegate — an object implementing a protocol with event handler methods. Disadvantage: "callback hell" (nested callbacks) and error handling complexity. NotificationCenter (iOS) and EventBus (Android) — broadcast event mechanisms useful for loosely coupled communication but leading to implicit dependencies.
Multithreading opens the door to high performance, but at the same time creates the risk of hard-to-catch bugs. The most common ones: Race Condition, Deadlock, Livelock and Starvation. Understanding these problems is an essential skill for any mobile developer.
Race Condition occurs when two or more threads simultaneously read and write the same data without synchronization. The result depends on which thread executes first. Classic example: two threads increment a counter. The "read → increment → write" operation is not atomic, so when executed simultaneously, one increment is "lost". The solution is to use atomic operations (AtomicInteger, AtomicReference) or locks (Mutex, Semaphore, synchronized).
Deadlock — a situation where each thread holds a resource and waits for a resource held by another thread. No thread can proceed. Conditions for occurrence: mutual exclusion, hold and wait, no preemption, circular wait. Prevention: establish a single lock acquisition order, use tryLock with timeout, apply Lock-Free algorithms (ConcurrentHashMap, CopyOnWriteArrayList).
Livelock — threads are not blocked but constantly "pass" resources to each other without doing useful work. Example: two people meet in a corridor and both step aside, moving in the same direction. Starvation — a thread does not get access to a resource because other threads constantly intercept it. Solution: fair locks, thread priorities with caution.
Synchronization primitives are used to prevent multithreading problems: Mutex (mutual exclusion), Semaphore (limiting the number of concurrent accesses), Lock (interface with tryLock), Synchronized (JVM-level lock), @MainActor (Swift — guarantees execution on the main thread). On Android ThreadPool is also available via Executors.newFixedThreadPool, newCachedThreadPool. However, manual pool management is the prerogative of legacy projects; in new projects it is better to use coroutines.
| Tool | Platform | Type | Features |
|---|---|---|---|
| DispatchQueue (GCD) | iOS | Task Queue | Serial/Concurrent, QoS priorities, Thread Pool managed by system |
| OperationQueue | iOS | Operation Queue | Dependencies, cancellation, maxConcurrentOperationCount |
| Coroutines + Flow | Android | Coroutines | Lightweight, structured concurrency, StateFlow, Channel |
| RxJava / RxKotlin | Android | Reactive Stream | Observable, Schedulers, rich set of operators |
| Combine | iOS | Reactive Stream | Publisher/Subscriber, SwiftUI integration |
| Async/Await + Task | iOS / Android | Async Model | Linear code, @MainActor, structured concurrency |
Frequently Asked Questions
Main Thread (UI thread) is responsible for rendering the interface and processing touches. Background Thread performs background tasks — data loading, computations, network work. Blocking the Main Thread causes interface freezing (ANR on Android, frozen UI on iOS).
Race Condition is when two threads simultaneously access shared data and the result depends on execution order. Avoided through synchronization: Mutex, Semaphore, Lock, Synchronized, @MainActor or atomic operations.
Coroutines are the modern standard for Android (JetBrains, supported by Google). RxJava/RxKotlin is a reactive approach with a rich set of operators. Coroutines are simpler for async calls, RxJava is more powerful for complex data streams. At IT Sectr we use Coroutines + Flow for new projects.
Deadlock is a mutual blocking where two threads wait for each other's resources. Livelock — threads are not blocked but constantly pass resources without doing useful work. Both problems are solved by proper lock ordering and timeouts.
DispatchQueue is an abstraction of Grand Central Dispatch (GCD) for thread management. Main Queue executes tasks on the main thread, Global Queues — on background threads. Serial Queue guarantees sequential execution, Concurrent Queue — parallel. In modern projects GCD is often replaced by Async/Await and Task.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.