28, Jul 2024
Swift: Concurrency
Concurrency is the ability to perform multiple tasks simultaneously, think showing a loading wheel while a document is being retrieved from a server. In Swift, there are several techniques for achieving concurrency, including Grand Central Dispatch (GCD), OperationQueues, and asynchronous programming with `async` and `await`. Let's explore these techniques and provide practical examples to help you understand how to write effective concurrent code which won't send your colleagues -or you- to the mad house.
Grand Central Dispatch (GCD)GCD is a powerful framework for performing asynchronous tasks. It provides a simple API for managing tasks and dispatching them to appropriate queues.
DispatchQueue.global(qos: .background).async {
// Perform background task
}
DispatchQueue.main.async {
// Update UI on the main thread
}
In many cases, you would want to execute something on ta global queue, then when it finishes, you'd want to update the UI, a common pitfall is attempting to execute UI operations on a queue other than the main thread. To do this properly, you'll need something like this:
DispatchQueue.global(qos: .background).async {
// Perform background task
// ....
// Background work done, now report back to the UI on the main queue
DispatchQueue.main.async {
// Update UI on the main thread
}
}
OperationQueuesOperationQueues provide a more object-oriented approach to concurrency. They allow you to create and manage operations, which are objects that represent tasks to be performed.
let queue = OperationQueue()
let operation1 = BlockOperation {
sleep(2)
print("One")
}
let operation2 = BlockOperation {
sleep(3)
print("Two")
}
queue.addOperations([operation1, operation2], waitUntilFinished: false)
print("Three")
`waitUntilFinished` is set to `false`, so this will print first an immediate "Three", a "One" after a 2 second delay and a "Two" after a 3 second delay. What do you think will happen if `waitUntilFinished` is set to `true` ?
**Asynchronous Programming with `async` and `await`**
Swift 5.5 introduced `async` and `await` keywords, which make asynchronous programming more concise and easier to read.
class User {
let name: String
init(data: Data) {
self.name = String(data: data, encoding: .utf8) ?? ""
}
}
func fetchUserData() async -> User? {
guard let url = URL(string: "https://api.example.com/user") else { return nil }
// Perform network request
guard let data = try? await URLSession.shared.data(from: url).0 else { return nil }
// Return parsed model
return User(data: data)
}
Task {
let user = await fetchUserData()
print(user?.name)
}
Choosing the Right TechniqueThe best concurrency technique for your application depends on several factors, including:
•
Complexity of tasks: For simple tasks, GCD or OperationQueues might be sufficient. For more complex tasks, asynchronous programming with `async` and `await` can be more appropriate. You may also choose the right approach based on the codebase you're already working with, for instance, if the entire app is built using
async/await, it's usually best to stick to the standard.
•
Performance requirements: GCD and OperationQueues can be highly performant, but asynchronous programming with
async/await can sometimes offer better performance.
•
Readability and maintainability: Asynchronous programming with
async/await can lead to more readable and maintainable code. However, I'll still advise the same idea from before, it's better to stick to the one standard that the team is used to rather than have a mishmash of different architectures.
Best Practices•
Avoid blocking the main thread: Long-running tasks should be performed on background threads to prevent the UI from freezing. I'd say this is actually the primary use of any concurrency library.
•
Use appropriate quality of service (QoS) classes: GCD provides different QoS classes to prioritize tasks based on their importance.
•
Handle errors gracefully: Always return a response, even if it's an error response. It's a very very common pitfall for developers to get lost in the complexity of concurrency that they focus on getting things to
just work and forget about that pesky error handling part, we'll do it later guys, we'll do it later :)
•
Test your concurrent code thoroughly: Ensure that your concurrent code works as expected under various conditions. Practice closing your eyes, and visualizing in your mind the different paths of concurrency. Take your time and walk it step by step, and pray. I've not come across time consuming bugs more than ones produced through concurrency malpractice.
Concurrency is a powerful tool that can help you write responsive and efficient applications. By understanding the different techniques available in Swift and following best practices, you can effectively use concurrency to improve the performance and user experience of your apps.