Concurrency & Multithreading in Swift
Hello Everyone,
Today we are going to delve into multithreading or concurrency in Swift.
let's see the below example before going into multithreading.
There is a small restaurant with 1 employee only. He is a chef as well as a waiter. He takes the order from customers and then goes to prepare the same. Once the order is ready then he delivers it to the customer and they only take new orders.
Another example could be.
There is a small restaurant with 1 chef and 1 waiter only. The waiter takes the order from the customer and then asks the chef to prepare the same. Once the order is ready, the waiter delivers it to the customer and then only takes new orders.
As we can see there are limited resources and one task is being executed at a time. We will see how can it be more efficient later with multithreading. Let's understand the basic concept of threading first.
What is a thread?
A thread is an execution context, which is all the information a CPU needs to execute a stream of instructions. I have found a very nice explanation about the same on stack overflow. Please see.
What is the process?
A thread is a context of execution, while a process is a bunch of resources associated with a computation. A process can have one or many threads.
We can see a detailed comparison between the two here.
What is multithreading?
From Wikipedia
In computer architecture, multithreading is the ability of a central processing unit (CPU) (or a single core in a multi-core processor) to provide multiple threads of execution concurrently, supported by the operating system.
Let's understand it with a restaurant example and see how multithreading solves the above-mentioned issue.
Imagine different tasks that need to be done simultaneously to ensure efficient operation: taking orders, cooking food, serving customers, and cleaning tables. Each of these tasks can be represented as a thread in a multithreaded system.
For instance, the waiter takes orders from multiple tables concurrently, without waiting for one order to be finished before moving to the next. Meanwhile, in the kitchen, chefs are cooking multiple dishes simultaneously, and the dishwasher is cleaning used dishes. Each of these tasks operates independently and concurrently, just like threads in a multithreaded program.
There are two important things to understand here. Concurrency & parallelism.
Please remember that both are different things that can be used to achieve multithreading.
What is concurrency?
Concurrency refers to the ability of a system to handle multiple tasks or processes simultaneously.
Let's understand the same restaurant example.
While the waiter is taking an order from a table, the chef can start preparing the ingredients for that order. As the waiter moves to another table to take another order, the chef can continue cooking the first order. This demonstrates concurrency because both tasks—taking orders and cooking—are happening simultaneously or overlapping in time, even though only one task is being executed at a given moment.
What is parallelism?
Parallelism refers to the simultaneous execution of multiple tasks or processes, typically to improve efficiency and speed. It comes with a cost most of the time.
Let's understand the same restaurant example.
Restaurant can hire more chefs and waiters so that they can serve more customers simultaneously. This solves the problem.
To some extent, restaurants can hire more employees to solve the issue. But, restaurants can not hire a chef & waiter as customer increases every time. They have to think, about how customers can be managed in a way that they do not have to hire more resources every time.
Here comes the term context switching or time slicing as part of the concurrency.
Context Switching: Context switching occurs when a processor (or in this case, a staff member) switches from executing one task to another. In the restaurant, context switching would happen when the waiter switches from taking an order at one table to taking an order at another table. Each time the waiter switches tables, they must mentally switch their context—remembering the order, preferences of the customers, etc. Context switching involves saving the state of the current task and loading the state of the new task.
For instance, if the waiter is taking an order at Table 1 and then suddenly needs to attend to Table 2 because of a customer's request, they have to switch their focus and remember the details of the new order while temporarily putting aside the details of the previous order. This context switching incurs a slight overhead in terms of mental effort and time, but it allows the waiter to handle multiple tasks concurrently.
Time Slicing: Time slicing, on the other hand, is a scheduling technique used to allocate CPU time to multiple tasks in a round-robin fashion. In the restaurant, time slicing could be compared to how the waiter divides their time among different tables. Instead of focusing on one table exclusively until all tasks are completed (such as taking orders, serving food, and clearing plates), the waiter allocates a small time slice to each table before moving on to the next one.
For example, the waiter might spend a few minutes taking orders at Table 1, then switch to serving food at Table 2, and then move on to clearing plates at Table 3, before returning to Table 1 to check if the customers need anything else. This way, each table cyclically receives attention, ensuring that no table is neglected for too long.
In summary, context switching refers to the process of mentally shifting focus between different tasks, while time slicing involves allocating CPU (or in this case, waiter) time to multiple tasks in a round-robin fashion to ensure fair and efficient utilization of resources. Both concepts are crucial for effective multitasking and resource management in dynamic environments like restaurants.
Now, we know what is multi-threading and how beneficial it is. let's see how to implement the same in iOS.
There are multiple ways to achieve the same.
Manual Thread.
We can create a manual thread in Swift to implement multithreading. Manual creating threads & managing gives more control over the execution because we have methods like start, cancel, delay, sleeps etc.
However, one has to be very careful while implementing this approach to solve multithreading. It is not recommended in Swift since we have some advanced and user-friendly solutions for the same.
I found a very good article that explains creating a thread, doing multi-threading, pros and cons of the same.
GCD(Grand Central dispatch)
Grand Central Dispatch (GCD) is a low-level API provided by Apple's operating systems for managing concurrent operations. In Swift, GCD is a powerful tool for managing tasks asynchronously and concurrently. It allows developers to execute tasks concurrently without having to manage threads directly.
OR
GCD is a queue-based API that allows developers to submit closures on the workers pools in FIFO order. The workers pool refers to the pool of threads.
For example of a restaurant. Suppose, we have 4 chefs in the restaurant. We got 2 orders from customers at the same time. The manager asks any available chef to prepare the order, since all 4 are free, and later 3 more orders come from the customers. The manager sees that only 2 chefs are free assigns new orders to them and waits for any chef to be available. Managers assign 5th order as soon as any chef becomes available.
We can refer to chefs as the pool of threads and the manager as the dispatch queue who is responsible for picking the chef.
So in terms of GCD, the dispatch queue decides which thread would be used for a task, not the developer.
We heard something new. Dispatch queue?
What is Dispatch Queue
A dispatch queue is a powerful mechanism for managing tasks in a multithreaded environment. It's a FIFO (First-In, First-Out) queue where tasks are executed concurrently but in a serial or concurrent manner, depending on the type of the queue. This means that tasks will always be picked in FIFO order in both cases serial and concurrent. However, execution would vary in both cases.
So how dispatch queue can be submitted to GCD?
A dispatch queue can be submitted synchronously or asynchronously. What does that mean? Is it not the same as serial or concurrent?
Please remember that both are different terms. For simplicity let's remember them as:
Order of execution vs Manner of execution
Order of execution decides how Job will be picked, serially or concurrently. While the manner of execution decides how the job will be executed.
Let's understand it with the same restaurant example.
Managers can decide if they take one order at a time, then wait for it to be prepared and then only take new orders Or take orders simultaneously. This is an order of execution.
Now, if a customer orders multiple dishes in one order. This is up to the chef if he prepares one dish at a time or multiple dishes. Like frying vegetables for one dish and while this is in progress, he cuts vegetables for a different recipe. This is called the manner of execution.
Again, there are different things here.
Now, we know the dispatch queue. There are three ways to access or create the queue.
However, most of the time we use a Global queue to perform the operations.
Now let's get into coding to understand the same better.
Example 1:(Main queue):
Please note that this is just for example to show how it works. Not recommended to perform tasks other than UI-related.
import UIKit
DispatchQueue.main.async {
if(Thread.isMainThread) {
print("It is main thread")
}
for dishe in 1...3 {
print(dishe)
}
}
for dishe in 4...6 {
print(dishe)
}
DispatchQueue.main.async {
print("9")
}
Here, we can see that we have submitted two async requests to the main queue which is serial. We are also printing from 4 to 6 without any queue.
Output would be:
4,5,6,It is main thread,1,2,3,9
Since we are printing 4 to 6 without any queue they are being executed on the main thread and will be the first to print.
Now comes these two queues. Please remember that these are async requests in a serial queue so that requests would be executed only after the other. As we can see 9 is only printed after 1,2,3.
So what is the benefit of async here? here async is tied to the inner execution of the async block. Let's see the closure of the first queue.
{
for dishe in 1...3 {
print(dishe)
}
}
Here, we have a simple loop to print so everything happens very quickly and this prints 1,2 & 3. Now let's think if we are downloading multiple images in this closure. Like below:
{
for dishe in 1...3 {
// create a request to download image
print("Creating the request")
// image downlaoded
print("image downlaoded")
}
}
There would be multiple requests for download in parallel running.
领英推荐
To understand better, we would be taking the example of downloading multiple images with serial/concurrent queues using sync and async operation. We will be taking multiple examples here.
Let's define a download method and URLs.
import UIKit
// Function to download an image from a URL asynchronously
func downloadImage(from urlString: String, completion: @escaping (UIImage?) -> Void) {
guard let url = URL(string: urlString) else {
completion(nil)
return
}
print("start download the image", urlString)
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data, let image = UIImage(data: data) else {
completion(nil)
return
}
completion(image)
}.resume()
}
// URLs of images to download
let imageUrls = [
"https://placehold.it/120x120&text=image1",
"https://placehold.it/120x120&text=image2",
"https://placehold.it/120x120&text=image3",
"https://placehold.it/120x120&text=image4"
]
Define a global Queue.
// Global dispatch queue (background)
var globalQueue = DispatchQueue.global()
Example 2(Global queue with async requests):
// Global dispatch queue (background)
let globalQueue = DispatchQueue.global()
// Download images asynchronously using global queue
for imageUrl in imageUrls {
globalQueue.async {
downloadImage(from: imageUrl) { image in
if let _ = image {
// Do something with the downloaded image
print("Downloaded image: \(imageUrl)")
} else {
print("Failed to download image: \(imageUrl)")
}
}
}
}
// Continue with other tasks while images are downloading asynchronously
print("Downloading images...")
Output:
start download the image https://placehold.it/120x120&text=image1
start download the image https://placehold.it/120x120&text=image2
start download the image https://placehold.it/120x120&text=image3
start download the image https://placehold.it/120x120&text=image4
Downloading images...
Downloaded image: https://placehold.it/120x120&text=image1
Downloaded image: https://placehold.it/120x120&text=image4
Downloaded image: https://placehold.it/120x120&text=image2
Downloaded image: https://placehold.it/120x120&text=image3
As we can see requests are picked in FIFO order, but downloading orders are different because requests run on different threads and execute concurrently
Example 3(Global queue with sync requests):
for imageUrl in imageUrls {
globalQueue.sync {
downloadImage(from: imageUrl) { image in
if let _ = image {
// Do something with the downloaded image
print("Downloaded image: \(imageUrl)")
} else {
print("Failed to download image: \(imageUrl)")
}
}
}
}
print("Downloading images...")
Output:
start download the image https://placehold.it/120x120&text=image1
start download the image https://placehold.it/120x120&text=image2
start download the image https://placehold.it/120x120&text=image3
start download the image https://placehold.it/120x120&text=image4
Downloading images...
Downloaded image: https://placehold.it/120x120&text=image1
Downloaded image: https://placehold.it/120x120&text=image2
Downloaded image: https://placehold.it/120x120&text=image3
Downloaded image: https://placehold.it/120x120&text=image4
As we can see requests are picked in FIFO order and processed one by one.
Example 3(Global queue with sync/async requests):
// Download images asynchronously using global queue
for i in 0...1 {
let imageUrl = imageUrls[i]
globalQueue.sync {
downloadImage(from: imageUrl) { image in
if let _ = image {
// Do something with the downloaded image
print("Downloaded image: \(imageUrl)")
} else {
print("Failed to download image: \(imageUrl)")
}
}
}
}
for i in 2...3 {
let imageUrl = imageUrls[i]
globalQueue.async {
downloadImage(from: imageUrl) { image in
if let _ = image {
// Do something with the downloaded image
print("Downloaded image: \(imageUrl)")
} else {
print("Failed to download image: \(imageUrl)")
}
}
}
}
We are looping from 0 to 1 index for sync and 2 to 4 async requests.
Output:
start download the image https://placehold.it/120x120&text=image1
start download the image https://placehold.it/120x120&text=image2
start download the image https://placehold.it/120x120&text=image3
start download the image https://placehold.it/120x120&text=image4
Downloading images...
Downloaded image: https://placehold.it/120x120&text=image1
Downloaded image: https://placehold.it/120x120&text=image2
Downloaded image: https://placehold.it/120x120&text=image3
Downloaded image: https://placehold.it/120x120&text=image4
Image1 & Image2 will always be the first to download because the sync operation blocks the current execution on the queue's thread until requests are executed successfully. Downloading order from image3 & image4 can be changed since they are async operations.
Example 4(Custom concurrent queue with sync/async requests):
We can create a custom concurrent queue like this.
// man concurrentual dispatch queue (background)
var globalQueue = DispatchQueue(label: "queuename", qos:.background, attributes: .concurrent)
Please note that if we do not specify attributes here then the queue becomes a serial queue.
The output would be just like example 3 here.
Example 5(Custom concurrent queue with sync/async requests):
let's define a custom serial queue.
// custom serial dispatch queue (background)
var globalQueue = DispatchQueue(label: "queuename")
Let's perform the same operation here.
Output would be.
start download the image https://placehold.it/120x120&text=image1
start download the image https://placehold.it/120x120&text=image2
start download the image https://placehold.it/120x120&text=image3
Downloading images...
start download the image https://placehold.it/120x120&text=image4
Downloaded image: https://placehold.it/120x120&text=image1
Downloaded image: https://placehold.it/120x120&text=image2
Downloaded image: https://placehold.it/120x120&text=image3
Downloaded image: https://placehold.it/120x120&text=image4
async/await(Swift 5.5)
In Swift 5.5, the introduction of async/await provides a powerful way to handle asynchronous code, making it more readable and maintainable. This feature simplifies writing and understanding code that performs asynchronous operations, such as network requests, file I/O, or long-running computations. Here’s a detailed explanation with examples.
What is async/await?
- `async`: A keyword used to mark a function that performs asynchronous work and can be suspended, allowing other code to run during the suspension.
- `await`: A keyword used to wait for the result of an asynchronous function. When the result is ready, execution resumes.
Benefits of async/await
1. Improved Readability: Asynchronous code written with async/await reads top-to-bottom, much like synchronous code, making it easier to understand.
2. Error Handling: async/await integrates seamlessly with Swift's try/catch error handling, providing a unified approach to managing errors.
3. Structured Concurrency: It helps to avoid callback hell and makes the flow of asynchronous code more predictable and structured.
How to Use async/await
1. Defining Asynchronous Functions:
To define an asynchronous function, use the async keyword before the function’s return type.
func fetchData(from url: URL) async throws -> Data {
// Function body
}
2. Calling Asynchronous Functions:
Use the await keyword to call an asynchronous function. This indicates that the current function will suspend until the awaited function completes.
let data = try await fetchData(from: someURL)
Example: Fetching Data from a URL
Here's a complete example demonstrating async/await in a network request.
1. Define an Asynchronous Function:
import Foundation
struct API {
func fetchData(from url: URL) async throws -> Data {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
}
2. Using the Asynchronous Function:
import Foundation
@main
struct MyApp {
static func main() async {
let api = API()
let url = URL(string: "https://api.example.com/data")!
do {
let data = try await api.fetchData(from: url)
print("Data fetched: \(data)")
} catch {
print("Failed to fetch data: \(error)")
}
}
}
Detailed Breakdown:
1. Defining the Asynchronous Function (`fetchData`):
- async keyword indicates the function performs asynchronous work.
- throws keyword indicates the function can throw errors.
- try await is used to call another asynchronous function (`URLSession.shared.data(from:)`), which might throw an error.
2. Calling the Asynchronous Function:
- The main function in the MyApp structure is marked with @main to make it the entry point.
- main function itself is async, enabling the use of await within it.
- try await is used to call fetchData, handling any errors that might occur.
Using Task to Run Asynchronous Code from a Synchronous Context:
Sometimes you need to call asynchronous code from a synchronous context. You can use Task for this purpose.
import Foundation
struct MyApp {
func run() {
let url = URL(string: "https://api.example.com/data")!
Task {
let api = API()
do {
let data = try await api.fetchData(from: url)
print("Data fetched: \(data)")
} catch {
print("Failed to fetch data: \(error)")
}
}
}
}
let app = MyApp()
app.run()
Error Handling with async/await
Error handling with async/await is straightforward and works seamlessly with Swift’s error handling mechanisms.
func loadData() async {
let url = URL(string: "https://api.example.com/data")!
do {
let data = try await fetchData(from: url)
print("Data fetched: \(data)")
} catch {
print("Error fetching data: \(error)")
}
}
In this example, any errors thrown by fetchData(from:) are caught in the catch block, making it easy to handle errors in a clean and readable manner.
Conclusion
The async/await syntax in Swift 5.5 greatly simplifies writing and managing asynchronous code. It improves code readability, maintains type safety, and integrates well with existing error handling mechanisms. By using async/await, developers can write more intuitive and maintainable asynchronous code, leading to better structured and more reliable applications.
Please note that most of the time we will be using a global queue in projects.
We are good till here. We are achieving what is required for an app to run smoothly. I know this is a big topic and would not be covered in one article.
We still have many important topics to learn here, Like Quality of service, dispatch group, data inconsistency & how to resolve data inconsistency.
We will see in the next article.
Thanks.