Concurrency & Multithreading in Swift

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.

  1. Manual Thread
  2. GCD(Grand Central dispatch)
  3. async/await(Swift 5.5)

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.

  1. Serial or concurrent queue
  2. Synchronous or Asynchronous

Now, we know the dispatch queue. There are three ways to access or create the queue.

  1. Main Queue: This is the serial queue provided by the GCD. It runs on the main thread. This is mainly used to update the UI.
  2. Global Queue: This is the concurrent queue provided by the GCD. This never runs on the main thread, only in rare circumstances main thread is used for the global queue.
  3. Custom Queue: We can manually create the serial or concurrent 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.



要查看或添加评论,请登录

Rishabh Yadav的更多文章

  • Understanding OAuth 2.0 and PKCE

    Understanding OAuth 2.0 and PKCE

    What is OAuth 2.0? OAuth 2.

  • Transforming Software Development: How OpenAI is Revolutionizing the Industry

    Transforming Software Development: How OpenAI is Revolutionizing the Industry

    OpenAI's advancements in artificial intelligence, particularly through large language models like GPT-4, are poised to…

  • Social Fun Game(Node.js & ReactJS)

    Social Fun Game(Node.js & ReactJS)

    As mentioned Earlier, I have started to work on Backend programming through Node.js.

  • State Management in React Native

    State Management in React Native

    Hello Everyone, Today, We are going to delve into state management in react native. This is always a hot discussion…

  • React Native new Architecture

    React Native new Architecture

    Hello everyone, Today, we're diving into the new architecture of React Native. But before we do, let's take a glance at…

  • Singleton: Good vs bad

    Singleton: Good vs bad

    The Singleton pattern has been debated in software engineering, with proponents and critics offering various…

  • Dependency Injection & Dependency Inversion Principle

    Dependency Injection & Dependency Inversion Principle

    Dependency injection is a design pattern where the dependencies of a class are provided from the outside rather than…

    2 条评论
  • React Server Component

    React Server Component

    Hi Everyone, Today, we are going to delve into the React server component. Project/Product managers often ask me about…

  • Closures in Swift

    Closures in Swift

    Closures in Swift are self-contained blocks of functionality that can be passed around and used in your code. They are…

  • MVVM in Swift UI

    MVVM in Swift UI

    Hello Everyone, Today, We are going to see a simple app with MVVM architecture. What is MVVM? Model-View-ViewModel…

社区洞察

其他会员也浏览了