DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Overcoming the Art Challenges of Staying Ahead in Software Development
  • Why You Might Need To Know Algorithms as a Mobile Developer: Emoji Example
  • Unlocking Language Models With Powerful Prompts
  • PostgresML: Extension That Turns PostgreSQL Into a Platform for AI Apps

Trending

  • How to Use AWS Aurora Database for a Retail Point of Sale (POS) Transaction System
  • Caching 101: Theory, Algorithms, Tools, and Best Practices
  • How to Introduce a New API Quickly Using Micronaut
  • Data Lake vs. Warehouse vs. Lakehouse vs. Mart: Choosing the Right Architecture for Your Business
  1. DZone
  2. Data Engineering
  3. Data
  4. A Developer’s Guide to Multithreading and Swift Concurrency

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.

By 
Andrei Trefilov user avatar
Andrei Trefilov
·
Dec. 09, 24 · Analysis
Likes (110)
Comment
Save
Tweet
Share
2.4K Views

Join the DZone community and get the full member experience.

Join For Free

Multithreading 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:

  1. 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.
  2. Execution Queue: A sequence of tasks that will be executed in the order they are added to the queue. These can be:
  3. Serial Queue: Executes tasks one at a time, in sequence.
  4. Concurrent Queue: Executes multiple tasks simultaneously.
  5. 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.
  6. 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.
  7. 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.
  8. Dispatch Group: Enables grouping multiple tasks together, tracking their completion, and performing actions once all tasks in the group are finished.
  9. 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.
  10. Swift Concurrency: A modern approach to asynchronous programming in Swift, featuring the async/awaitsyntax, Task, and TaskGroup. 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.
Swift
 
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.

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

  1. .userInteractive: Maximum priority, used for tasks that the user is actively interacting with and require immediate completion (e.g., animations or UI updates).
  2. .userInitiated: High priority for tasks that the user starts and expects to complete quickly (e.g., fetching data needed right away).
  3. .default: Standard priority, used for general tasks that don’t require maximum or minimum QoS levels.
  4. .utility: Medium priority, suitable for long-running tasks that are not critical but still important (e.g., file downloads).
  5. .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 the statusesQueue with the .barrierflag. 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.
Swift
 
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 of OperationQueue 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 to mainQueue are executed sequentially on the main thread.
Swift
 
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.
Swift
 
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.
Swift
 
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 via group, and the method provides an efficient way to combine the results of all tasks once they complete.
Swift
 
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.

Data structure Swift (programming language) Task (computing)

Opinions expressed by DZone contributors are their own.

Related

  • Overcoming the Art Challenges of Staying Ahead in Software Development
  • Why You Might Need To Know Algorithms as a Mobile Developer: Emoji Example
  • Unlocking Language Models With Powerful Prompts
  • PostgresML: Extension That Turns PostgreSQL Into a Platform for AI Apps

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!