Multithreading and Concurrency in Mobile Development: What It Is, Principles and How It Works

Author: IT Sectr Published: 2026-03-12 Reading time: 13 min

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

  • Main Thread — the only thread for working with the UI; all other tasks are moved to Background
  • GCD and OperationQueue — the main multithreading mechanisms in iOS
  • Coroutines and Flow — the modern standard for asynchrony in Kotlin/Android
  • RxJava, RxSwift and Combine — reactive frameworks for working with data streams
  • Race Condition, Deadlock and Livelock — classic multithreading problems requiring synchronization
  • The choice of tool depends on the platform and task complexity: Async/Await is enough for simple calls, Rx or Combine for complex streams

What is Multithreading?

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 and Background Threads

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".

GCD and OperationQueue (iOS)

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 and Flow (Kotlin)

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:

kotlin
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()
        }
    }
}

Rx and Combine

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 Problems (Race Condition, Deadlock)

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

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

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 and Starvation

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 Tools

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)iOSTask QueueSerial/Concurrent, QoS priorities, Thread Pool managed by system
OperationQueueiOSOperation QueueDependencies, cancellation, maxConcurrentOperationCount
Coroutines + FlowAndroidCoroutinesLightweight, structured concurrency, StateFlow, Channel
RxJava / RxKotlinAndroidReactive StreamObservable, Schedulers, rich set of operators
CombineiOSReactive StreamPublisher/Subscriber, SwiftUI integration
Async/Await + TaskiOS / AndroidAsync ModelLinear code, @MainActor, structured concurrency

Frequently Asked Questions

How is Main Thread different from Background Thread?

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).

What is Race Condition and how to avoid it?

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 or RxJava: which to choose for Android?

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.

What are Deadlock and Livelock?

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.

Why is DispatchQueue needed in iOS?

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

  • Main Thread — UI only; all other operations go to Background
  • GCD and OperationQueue — the foundation of multithreading on iOS; Async/Await — the modern alternative
  • Coroutines and Flow — the standard for Android; structured concurrency prevents leaks
  • RxJava, RxSwift, Combine — reactive frameworks for complex data streams
  • Race Condition and Deadlock — the main problems; solved by locks and proper resource acquisition order
  • Thread Pool is managed by the system (GCD) or framework (coroutines); manual thread creation is not recommended
  • The choice of tool depends on the platform: Coroutines for Android, GCD/Combine for iOS

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.

Discuss the project