A Developer’s Guide to Multithreading and Swift Concurrency
Learn the basics of multithreading and how Swift Concurrency simplifies writing efficient, parallel code with async/await, tasks, and structured concurrency.
Join the DZone community and get the full member experience.
Join For FreeMultithreading is a complex yet essential topic in software development. It allows programs to perform multiple tasks simultaneously, which is critical for creating efficient and responsive applications. However, managing multiple threads and ensuring their smooth interaction can be challenging, especially when it comes to avoiding conflicts or maintaining synchronization.
This article is designed to give you a high-level overview of multithreading and the tools available to work with it. We’ll explore the key concepts and features that help developers handle concurrent tasks more effectively. Whether you’re just getting started or looking for a quick refresher, this guide will provide a clear starting point for understanding and working with multithreading.
What Is Multithreading?
Multithreading is the ability of a program to perform multiple tasks simultaneously by distributing them across multiple threads. Each thread represents an independent sequence of execution that works in parallel with others. This allows applications to be more efficient and responsive, especially when performing complex operations such as data processing or network requests, where using the main thread might cause delays in the user interface.
Multithreading is often used for asynchronous tasks — tasks that run in parallel without blocking the main thread. In Swift, the main thread is responsible for handling the user interface (UI), so any operations that might slow it down should be executed in the background.
In modern applications, particularly mobile ones, multithreading is an essential part of performance optimization. For example, if an app needs to load an image from the network, performing this task on the main thread could slow down the interface and cause it to "freeze." Instead, the task can be sent to a background thread, keeping the main thread free to handle user interactions.
Multithreading enables multiple operations to run simultaneously. However, it's important to manage threads and synchronization carefully to avoid data conflicts, especially when multiple threads access the same resources.
Here are the key multithreading terms that developers work with in Swift:
- Global Queue: System-managed task queues that distribute tasks among available threads. They are designed for executing low-priority tasks and release resources as soon as they are no longer needed.
- Execution Queue: A sequence of tasks that will be executed in the order they are added to the queue. These can be:
- Serial Queue: Executes tasks one at a time, in sequence.
- Concurrent Queue: Executes multiple tasks simultaneously.
- Asynchronous Execution: A method of execution where a task is handed off to another thread, freeing up the main thread for other tasks. Once the task is completed, the result can be returned to the main thread to update the UI.
- Synchronous Execution: A method of execution where a task blocks the current thread until it is completed. This is rarely used as it can block the interface and degrade the app's responsiveness.
- Semaphore: A mechanism for controlling access to resources in multithreaded environments, allowing you to limit the number of threads that can perform certain tasks simultaneously.
- Dispatch Group: Enables grouping multiple tasks together, tracking their completion, and performing actions once all tasks in the group are finished.
- Operations and Operation Queues (NSOperation and NSOperationQueue): High-level objects for task management that offer more flexibility than GCD (Grand Central Dispatch), such as the ability to pause, cancel tasks, or add dependencies between them.
- Swift Concurrency: A modern approach to asynchronous programming in Swift, featuring the
async/await
syntax,Task
, andTaskGroup
. It simplifies writing asynchronous code and avoids the complexities of manual thread management.
Multithreading Issues
Race Condition: A situation where multiple threads simultaneously access the same variable or resource, and the outcome depends on the order of operations. This can lead to incorrect and unpredictable results.
Deadlock: A situation where multiple threads block each other, waiting for resources held by the other threads. This results in a complete halt of the program, as no thread can proceed.
Resource Contention: When multiple threads compete for limited resources, such as CPU time or memory access, it can reduce the program's performance and cause significant delays.
Now let’s look at two example tasks. The first task will demonstrate the basic functionality of a tool. The second task will address a more complex scenario using the concepts described above.
Task 1: Loading an Image in the Background
Objective
Imagine you need to download an image from a network using a URL, but it's important that this operation does not block the main thread (so the user can interact with the interface). After the image is downloaded, the result should be passed back to the main thread to be displayed in the interface (e.g., in a UIImageView
).
Key Points
- The image download should happen on a background thread.
- The result must be returned to the main thread.
- The main thread should not be blocked.
Task 2: Parallel Download of Multiple Images
Objective
Suppose you have a list of URLs for downloading multiple images. You need to:
- Start downloading all images in parallel.
- Track the download status of each image.
- Safely update the download status for each image as it completes.
- Once all images are downloaded, pass an array of the results to the main thread, ensuring that the order of the images in the array matches the order of the URLs.
Key Points
- Parallel downloading of images.
- Safe tracking of the download status for each image.
- Ensuring the order of images in the output array matches the order of the URLs.
- The result is passed to the main thread.
Thread Management Techniques
These techniques can help optimize task execution and ensure efficient use of system resources in multithreaded applications.
Thread
A thread is a basic object for managing threads in Swift. It allows you to create and manage threads manually but requires significant control and resource management.
Task 1
performSelector(onMainThread:with:waitUntilDone:)
: Executes a selector (method) on the main thread.detachNewThread
: Launches a block of code in a separate thread.
class ThreadImageLoaderTask1 {
func loadImageFrom(url: URL) {
// move the task to a separate thread
Thread.detachNewThread { [weak self] in
// load the image
let data = try? Data(contentsOf: url)
if let data, let image = UIImage(data: data) {
// process the result on the main thread
Thread.performSelector(
onMainThread: #selector(self?.handle(_:)),
with: image,
waitUntilDone: false
)
} else {
// handle the error
}
}
}
@objc
private func handle(_ image: UIImage?) {
// handle the final result
}
}
Task 2
NSLock: An Objective-C class that provides a straightforward way to manage access to critical sections of code in multithreaded applications. It ensures that multiple threads cannot execute the same code at the same time, making access to shared resources safe and reliable.
class ThreadImageLoaderTask2 {
private var images: [UIImage?] = []
private var statuses: [URL: LoadingStatus] = [:]
private let lock = NSLock()
func loadImages(urls: [URL]) {
images = Array(repeating: nil, count: urls.count)
for (index, url) in urls.enumerated() {
// start loading each URL in a new thread
Thread.detachNewThread { [weak self] in
self?.updateStatus(for: url, status: .loading)
let data = try? Data(contentsOf: url)
if let data, let image = UIImage(data: data) {
// safely save the loading result
self?.lock.lock()
self?.images[index] = image
self?.lock.unlock()
self?.updateStatus(for: url, status: .finished)
Thread.performSelector(
onMainThread: #selector(self?.handle),
with: nil,
waitUntilDone: false
)
} else {
// handle the error
self?.updateStatus(for: url, status: .error)
}
}
}
}
func getStatus(for url: URL) -> LoadingStatus {
// safely retrieve the status for a specific image
lock.lock()
let status = statuses[url]
lock.unlock()
return status ?? .ready
}
private func updateStatus(for url: URL, status: LoadingStatus) {
// safely update the status
lock.lock()
statuses[url] = status
lock.unlock()
}
@objc
private func handle() {
// safely check if all images are loaded
lock.lock()
let imagesAreReady = statuses.allSatisfy({
$0.value == .finished || $0.value == .error
})
lock.unlock()
if imagesAreReady {
// handle the final result
}
}
}
// loading statuses
// will be used in further examples
enum LoadingStatus {
case ready // ready to load
case loading // currently loading
case finished // successfully loaded
case error // an error occurred
}
GCD
Grand Central Dispatch is a framework for managing threads using queues, making it easy to execute tasks asynchronously and in parallel without manually creating threads.
Task 1
DispatchQueue.global(qos: .background).async
: Creates a task to be executed asynchronously in a global queue on a background thread with the specified Quality of Service (QoS) level .background.DispatchQueue.main.async
: Executes a task asynchronously on the main queue.
class GCDImageLoaderTask1 {
func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
// move the task to a separate background thread
DispatchQueue.global(qos: .background).async {
if let data = try? Data(contentsOf: url), let image = UIImage(data: data) {
// process the result on the main thread
DispatchQueue.main.async {
completion(image)
}
} else {
// handle the error
}
}
}
}
Other QoS Levels (Quality of Service):
.userInteractive
: Maximum priority, used for tasks that the user is actively interacting with and require immediate completion (e.g., animations or UI updates)..userInitiated
: High priority for tasks that the user starts and expects to complete quickly (e.g., fetching data needed right away)..default
: Standard priority, used for general tasks that don’t require maximum or minimum QoS levels..utility
: Medium priority, suitable for long-running tasks that are not critical but still important (e.g., file downloads)..background
: Lowest priority, used for tasks that are not visible to the user and can take longer to complete (e.g., backups or analytics).
Task 2
DispatchQueue(label: "statuses", attributes: .concurrent)
: Creates a new queue named "statuses" with the attribute.concurrent
, meaning the queue will execute tasks in parallel.statusesQueue.sync(flags: .barrier)
: Executes a task synchronously on thestatusesQueue
with the.b
arrier
flag. This ensures the task runs exclusively — waiting for all currently running tasks to finish first, and preventing new tasks from starting until it completes.DispatchGroup
: Allows you to group multiple asynchronous tasks together, track their completion, and perform an action once all tasks in the group are finished.
class GCDImageLoaderTask2 {
private var statuses: [URL: LoadingStatus] = [:]
// queue for handling statuses
private let statusesQueue = DispatchQueue(label: "statuses", attributes: .concurrent)
func loadImages(from urls: [URL], completion: @escaping ([UIImage?]) -> Void) {
let group = DispatchGroup() // create a group for parallel requests
var images = Array<UIImage?>(repeating: nil, count: urls.count)
for (index, url) in urls.enumerated() {
group.enter() // mark entry for each download process
DispatchQueue.global(qos: .background).async { [weak self] in
// safe access to statuses
self?.statusesQueue.sync(flags: .barrier) {
self?.statuses[url] = .loading
}
if let data = try? Data(contentsOf: url), let image = UIImage(data: data) {
self?.statusesQueue.sync(flags: .barrier) {
self?.statuses[url] = .finished
}
images[index] = image
} else {
// handle the error
self?.statusesQueue.sync(flags: .barrier) {
self?.statuses[url] = .error
}
}
group.leave() // notify that the thread has finished its work
}
}
// once all threads are complete, return the result on the main thread
group.notify(queue: .main) {
completion(images)
}
}
func getStatus(for url: URL) -> LoadingStatus {
return statusesQueue.sync {
return statuses[url] ?? .ready
}
}
}
OperationQueue
OperationQueue is a higher-level abstraction over GCD that provides a more sophisticated interface for managing task dependencies and priorities, making it easier to coordinate complex operations.
Task 1
backgroundQueue
: An instance ofOperationQueue
that is created by default as a background queue. It executes tasks asynchronously and can run multiple tasks simultaneously (depending on system defaults) since it is not tied to the main thread.mainQueue
: A queue bound to the application's main thread. All tasks added tomainQueue
are executed sequentially on the main thread.
class OperationQueueImageLoaderTask1 {
func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
let backgroundQueue = OperationQueue()
let mainQueue = OperationQueue.main
// create a block operation to execute in the background queue
let downloadImageOperation = BlockOperation {
if let data = try? Data(contentsOf: url), let image = UIImage(data: data) {
// upon completion of the download, call completion on the main queue
mainQueue.addOperation {
completion(image)
}
} else {
// handle the error
}
}
// add the operation to the background queue
backgroundQueue.addOperation(downloadImageOperation)
}
}
Task 2
addDependency
: Allows you to set a dependency between operations, ensuring that the current operation does not start until the specified dependent operation is complete.
class OperationQueueImageLoaderTask2 {
private let statusesQueue = DispatchQueue(label: "statuses", attributes: .concurrent)
private var statuses: [URL: LoadingStatus] = [:]
func loadImages(from urls: [URL], completion: @escaping ([UIImage?]) -> Void) {
let backgroundQueue = OperationQueue()
let mainQueue = OperationQueue.main
var images = Array<UIImage?>(repeating: nil, count: urls.count)
// final operation that will depend on all download operations
let completionOperation = BlockOperation {
completion(images)
}
for (index, url) in urls.enumerated() {
let downloadOperation = BlockOperation { [weak self] in
self?.statusesQueue.sync(flags: .barrier) {
self?.statuses[url] = .loading
}
if let data = try? Data(contentsOf: url), let image = UIImage(data: data) {
self?.statusesQueue.sync(flags: .barrier) {
self?.statuses[url] = .finished
}
images[index] = image
} else {
// handle the error
self?.statusesQueue.sync(flags: .barrier) {
self?.statuses[url] = .error
}
}
}
// add dependency on the completion operation
completionOperation.addDependency(downloadOperation)
backgroundQueue.addOperation(downloadOperation)
}
// add the final operation to the main queue when ready
mainQueue.addOperation(completionOperation)
}
func getStatus(for url: URL) -> LoadingStatus {
return statusesQueue.sync {
return statuses[url] ?? .ready
}
}
}
Task (async/await)
The Task
(async/await) is a modern approach to asynchronous programming in Swift. It simplifies managing asynchronous tasks using the async/await
syntax, improving code readability and maintainability.
Task 1
Task
: Creates a new asynchronous task in the current context. The task starts immediately and allows you to run asynchronous code within the block.MainActor.run
: Executes the given block of code on the main thread, ensuring operations are performed in a safe main-thread context.async
: Marks a function as asynchronous, indicating it can pause execution to await the completion of other asynchronous tasks.await
: Used inside an asynchronous function to pause execution until an asynchronous task completes. This enables writing asynchronous code in a sequential and readable manner.
class TaskImageLoaderTask1 {
// Example 1
func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
Task { // start a task in the background thread
if let data = try? Data(contentsOf: url), let image = UIImage(data: data) {
await MainActor.run { // process the result on the main thread
completion(image)
}
} else {
// handle the error
}
}
}
// Example 2. More modern
func loadImage(from url: URL) async throws -> UIImage? {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw NSError(domain: "InvalidImageData", code: 0, userInfo: nil)
}
return image
}
}
Task 2
actor
: A mechanism for isolating state. Actors ensure that access to their state occurs on a single thread at a time, helping to prevent race conditions and making the code thread-safe.withTaskGroup(of: (Int, UIImage?).self) { group in }
: A method that creates a group of asynchronous tasks, allowing them to execute in parallel. Tasks can be added to the group viagroup
, and the method provides an efficient way to combine the results of all tasks once they complete.
actor IgagesLoadingStatuses { // mechanism for isolating state
private var statuses: [URL: LoadingStatus] = [:]
func update(status: LoadingStatus, for url: URL) {
statuses[url] = status
}
func getStatus(for url: URL) -> LoadingStatus {
statuses[url] ?? .ready
}
}
class TaskImageLoaderTask2 {
private var images: [UIImage?] = []
private var statuses = IgagesLoadingStatuses()
private let lock = NSLock()
func loadImages(urls: [URL]) async -> [UIImage?] {
images = Array(repeating: nil, count: urls.count)
return await withTaskGroup(of: (Int, UIImage?).self) { group in
for (index, url) in urls.enumerated() {
// launch tasks for each URL
group.addTask { [weak self] in
do {
let image = try await self?.loadImage(from: url)
self?.images[index] = image
await self?.statuses.update(status: .finished, for: url)
return (index, image)
} catch {
// handle the error
await self?.statuses.update(status: .error, for: url)
return (index, nil)
}
}
}
for await (index, image) in group {
images[index] = image
}
return images
}
}
func getStatus(for url: URL) async -> LoadingStatus {
await statuses.getStatus(for: url)
}
private func loadImage(from url: URL) async throws -> UIImage? {
await statuses.update(status: .loading, for: url)
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw NSError(domain: "InvalidImageData", code: 0, userInfo: nil)
}
return image
}
}
// example usage
@MainActor
func loadImages() {
let loader = TaskImageLoaderTask2()
Task {
let images = await loader.loadImages(urls: [/*...*/])
// use images
}
}
In addition to these tools, there is another one — pthread — a low-level interface for working with threads that provides capabilities for thread management and synchronization based on POSIX. Unlike higher-level APIs such as GCD or OperationQueue
, using pthread
requires detailed control over each thread, including its creation, termination, and resource management. This makes pthread
more complex to use, as the developer must account for all aspects of multithreaded operations manually.
Conclusion
Mastering multithreading is essential for optimizing modern applications. Using the tools discussed and understanding synchronization and queues will enable you to handle multiple tasks concurrently, which is key to building responsive and efficient apps. Although challenges can arise, with the right approach and techniques, developers can harness the full power of multithreading.
Opinions expressed by DZone contributors are their own.
Comments