Skip to content

Instantly share code, notes, and snippets.

@avii-7
Last active July 16, 2024 15:50
Show Gist options
  • Save avii-7/e160a3ea5cbd3631a091e733957f06d0 to your computer and use it in GitHub Desktop.
Save avii-7/e160a3ea5cbd3631a091e733957f06d0 to your computer and use it in GitHub Desktop.
Multi-Threading - Swift

Dispatch Group

It is used to manage the execution of multiple tasks concurrently, can be used in a situation where you need to aggregate the results of these tasks before proceeding further.

DispatchGroup is not the best choice when one task is dependent on other.

Initialization:

let group = DispatchGroup()

Entering the Group: You need to call group.enter() before starting an asynchronous task.

group.enter()
DispatchQueue.global().async {
    // Perform your async task here
    group.leave() 
    // Call leave() when the task is completed
}

Waiting for All Tasks to Complete: There are two ways to wait for the tasks to complete:

Synchronous Wait:

group.wait()
// This blocks the current thread until all tasks in the group have completed

Asynchronous Wait:

group.notify(queue: .main) {
    // This block is executed when all tasks in the group have completed.
    print("All tasks are completed")
}

DispatchQueue Basics

Introduction

DispatchQueue is an object that manages the execution of tasks serially or concurrently on main thread or on a background thread.

Serial Custom DispatchQueue

A custom DispatchQueue ensures that tasks are executed one at a time in the order they are added.

let queue = DispatchQueue(label: "com.avii.example")

Using sync on Custom Serial Queue

Defination: When using sync, the calling thread blocked until the submitted block completes.

Purpose: Ensures tasks are performed sequentially and allows for thread-safe access to shared resources.

Below we created an example, that demonstrate, how sync is useful to prevent race conditions.

class ThreadSafeExample {
    
    private(set) var balance = 100
    
    private let queue = DispatchQueue(label: "com.avii.example")
    
    func decrement(by value: Int) -> String {
      queue.sync {
            if balance >= value {
                Thread.sleep(forTimeInterval: 4)
                balance -= value
                return "Success"
            }
            
            return "Insufficient Balance"
        }
    }
    
    func get() -> Int {
        balance
    }
}

let obj = ThreadSafeExample()

DispatchQueue.global().async {
    print(obj.decrement(by: 100))
    print(obj.balance)
}

DispatchQueue.global().async {
    print(obj.decrement(by: 50))
    print(obj.balance)
}



As mentioned above, DispatchQueue initializer creates a serial queue.

let queue = DispatchQueue(label: "com.avii.example")

Then keep in mind that execution of multiple DispatchWorkItems will happen sequentially.

Like, in the below example, we are printing numbers inside DispatchWorkItem block in the duration of 1 second and also scheduling a block execution after 3 seconds to cancel our other block.

let workItem = DispatchWorkItem {
    for i in 1...10 {
        print(i, workItem.isCancelled)
        Thread.sleep(forTimeInterval: 1)
    }
}

let queue = DispatchQueue(label: "com.avii.testing1")

queue.async(execute: workItem)

queue.asyncAfter(deadline: .now() + .seconds(3)) {
    print("I'm Cancelling now")
    workItem.cancel()
}

Output:

1 false
2 false
3 false
4 false
5 false
6 false
7 false
8 false
9 false
10 false
I'm Cancelling now

So what happen is, first our system busy in printing numbers and when it become free it start executing our another block from queue.

Thereby "I'm cancelling now" message printed after first block finishes. Then it waits for 3 seconds before printing "I'm Cancelling now" message.

What problem Concurrent DispatchQueue Barrier flag solves ?

Problem:
Suppose there is a resource that is shared between mutiple threads. One thread is performing write operation (which is time consuming) and can take few seconds to complete. At the same time if one thread tried to access that resource, it will get unpredictable reesult (like mismatch between number of items).

To solve this problem you may think to use serial queue, in which you will wrap your read operations into queue's sync/async blocks and will wait until the write operations completes before executing read operations.

Let's proceed with an example:

final class Messenger {
    
    static let shared = Messenger()
    
    private init() { }
    
    private var array = [String]()

    // Creating a serial queue
    private let queue = DispatchQueue(label: "com.avii.mt")
    
    // Simulating the posting of a message using Thread.sleep.
    func postMessage(_ message: String) {
        queue.async { [weak self] in
            Thread.sleep(forTimeInterval: 4)
            self?.array.append(message)
        }
    }
    
    func getLastMessage(_ completion: @escaping ((String?) -> Void)) {
        queue.async { [weak self] in
            completion(self?.array.last)
        }
    }
}

// Appending operation using #Thread 1
DispatchQueue.global().async {
    Messenger.shared.postMessage("M1")
}

// Reading operation using #Thread 2
DispatchQueue.global().async {
    Messenger.shared.getLastMessage { message in
        print(message ?? "nil")
    }
}

In the above example, we used used serial queue.

But this approach will slow down my read operations. Now my read operations will execute sequentially, with one read operation having to wait for another to complete.


In that case, barrier flag can help us to solve this problem.

Using this flag, we can prevent the read operations from happening until the write operation has completed.

Example:

class Messenger {
    
    static let shared = Messenger()
    
    private init() { }
    
    private var array = [String]()
    
    private let queue = DispatchQueue(label: "com.avii.mt", attributes: .concurrent)
    
    // Time consuming task.
    func postMessage(_ message: String) {
        queue.async(flags: .barrier) { [weak self] in
            Thread.sleep(forTimeInterval: 4)
            self?.array.append(message)
            print("appending done !")
        }
    }
    
    func getLastMessage(_ completion: @escaping ((String?) -> Void)) {
        queue.async { [weak self] in
            completion(self?.array.last)
        }
    }
}

Barrier flag Definition form Apple:
When submitted to a concurrent queue, a work item with this flag acts as a barrier. Work items submitted prior to the barrier execute to completion, at which point the barrier work item executes. Once the barrier work item finishes, the queue returns to scheduling work items that were submitted after the barrier.


References: https://www.avanderlee.com/swift/concurrent-serial-dispatchqueue/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment