Xcoding with Alfian

Software Development Videos & Tutorials

Using Dispatch Group & Semaphore to Group Async Tasks

Alt text

Grand Central Dispatch (GCD) is a framework provided by Apple that was released in 2009 with OS X Snow Leopard & iOS 4. It provides easy to use API for the developers to to run background tasks by creating queue in serial or concurrent style without managing threads by themselves.

GCD abstracts the assignment of threads for computation into dispatch queue. Developers only need to create their own dispatch queue or they can use Apple provided built in global dispatch queue with several built-in Quality of Service (QoS) from user interactive, user initiated, utility, and background. GCD will handle the thread assignment in a thread pool automatically.

There are some instances when we as a developer need to perform multiple batch asynchronous tasks in the background, and then receive the notification when all the job is completed in the future. Apple provides DispatchGroup class that we can use to do this kind of operation. Here are the brief summary of what the DispatchGroup is by Apple.

Groups allow you to aggregate a set of tasks and synchronize behaviors on the group. You attach multiple work items to a group and schedule them for asynchronous execution on the same queue or different queues. When all work items finish executing, the group executes its completion handler. You can also wait synchronously for all tasks in the group to finish executing.

While the DispatchGroup can also be used to wait synchronously for all tasks in the group to finish executing, we won’t doing this in this tutorial.

We’ll also will use the DispatchSemaphore to limit the number of concurrent simultaneous tasks that we execute in a queue. According to Apple DispatchSemaphore is:

An object that controls access to a resource across multiple execution contexts through use of a traditional counting semaphore.

Here are also some of the real use cases that we might encounter as an developer:

  1. Performing multiple network requests that depends on the completion of all the other requests before continue.
  2. Performing multiple videos/images processing tasks in the background .
  3. Download/upload of files in the background simultaneously.

What we will build


We will be exploring on how we can leverage DispatchGroup & DispatchSemaphore by creating a simple project that will perform a simulation of simultaneous background downloads, when all the download tasks completed successfully, we will display successful alert message in the UI. It also has several capabilities such as:

  1. Set the total number of download tasks.
  2. Randomize the download time of each task.
  3. Set how many concurrent tasks can be run a queue simultaneously.

To begin, please download the starter project from the GitHub repository link at alfianlosari/DispatchGroupExample-Starter.

The Starter Project

The starter project contains all the view controller , cells that has been prepared before so we can focus on how to use the dispatch group & dispatch semaphore. We will simulate the action of performing multiple downloading of files in the background by using the dispatch group. We’ll also look into how we can use dispatch semaphores to limit the number of simultaneous concurrent download to a specific number.

The Download Task

The DownloadTask class will be used to simulate the download of a file in the background. Here are breakdown of the class:

  1. It has the TaskState enum as the property, the 3 cases are pending , inProgress with int as associated value, and completed . This will be used to manage the state of the download task. The initial state is pending .
  2. The intializer accepts an identifier, and the state update handler closure . The identifier can be used to identify the task within the other tasks, while the closure will be invoked as a callback whenever the state gets updated.
  3. The progress variable will be used to track the current completion progress of the download. This will be updated periodically when the download task starts.
  4. The startTask method that is currently empty. We will add the code to perform the task inside a DispatchGroup with semaphore later.
  5. The startSleep method will be used to make the thread sleep for specified duration to simulate the downloading of a file.

The View Controller

Alt text

The main JobListViewController consists of 2 table view, one is for displaying download tasks and one is for is displaying completed tasks. There are also several sliders and a switch that we can configure. Here are the breakdown of the class:

  1. The CompletedTasks and DownloadTasks arrays. These arrays will store all the download tasks and completed tasks. The top table view displays the current download tasks, while the bottom table view displays the completed download tasks.
  2. The SimulationOption struct where we store the configuration of the app such as the number of tasks, whether the download time is randomized, and number of simultaneous concurrent tasks in a queue.
  3. The TableViewDataSource cellForRowAtIndexPath will dequeue the ProgressCell , and pass the DownloadTask to configure the cell UI depending of the state.
  4. The tasksCountSlider. This slider will determine the number of tasks that we want to simulate in a dispatch group.
  5. The maxAsyncTasksSlider. This slider will determine the number of maximum concurrent tasks that will run in a dispatch group. For example, given a 100 download tasks, we want our queue to proceed only 10 downloads simultaneously. We will use DispatchSemaphore to limit the number of maximum resource that will be utilized.
  6. The randomizeTimeSwitch. This switch will determine whether to randomize the download time of each download task.

Let’s begin by simulating the operation when user taps on the start bar button item that will trigger the startOperation selector that is currently empty.

Creating DispatchQueue, DispatchGroup, & DispatchSemaphore

To create all the the instances of those 3 we can just instantiate them with their respective class, then assign it to a variable like so. The DispatchQueue intializer is given a unique identifier (using a reverse domain dns name as the convention), then we set the attributes to concurrent so we can perform multiple jobs in parallel asynchronously. We also set DispatchSemaphore value using the maximumAsyncTaskCount to limit the number of simultaneous download task. At last, we also make sure to disable the user interaction of all the buttons, sliders, and switch when the operation starts.

@objc func startOperation() {
      downloadTasks = []
      completedTasks = []
      
      navigationItem.rightBarButtonItem?.isEnabled = false
      randomizeTimeSwitch.isEnabled = false
      tasksCountSlider.isEnabled = false
      maxAsyncTasksSlider.isEnabled = false
      
      let dispatchQueue = DispatchQueue(label: "com.alfianlosari.test", qos: .userInitiated, attributes: .concurrent)
      let dispatchGroup = DispatchGroup()
      let dispatchSemaphore = DispatchSemaphore(value: option.maxAsyncTasks)
  
}

Creating The Download Tasks & Handling State Update

Next, we just create the tasks based on the number of the maximumJob count from the option property. Each DownloadTask is instantiated with an identifier, then in the task update state closure, we pass the callback. Here are the breakdown of the callback implementation:

  1. Using the task identifier, we retrieve the index of the task from downloadTask array.
  2. In the completed state, we just remove the task from the downloadTasks array, then insert the task into the completedTasks array index zero. The downloadTasks and completedTasks has a property observer that will trigger the reloadData method in their respective table view.
  3. In the inProgress state, we retrieve the cell from the downloadTableView using the cellForIndexPath: method, then we invoke the configure method passing the new state. At last, we also trigger the tableView beginUpdates and endUpdates in case the the height of the cell changed.
@objc func startOperation() {
       // ...
       
       downloadTasks = (1...option.jobCount).map({ (i) -> DownloadTask in
           let identifier = "\(i)"
           return DownloadTask(identifier: identifier, stateUpdateHandler: { (task) in
               DispatchQueue.main.async { [unowned self] in
                   
                   guard let index = self.downloadTasks.indexOfTaskWith(identifier: identifier) else {
                       return
                   }
                   
                   switch task.state {
                   case .completed:
                       self.downloadTasks.remove(at: index)
                       self.completedTasks.insert(task, at: 0)
                       
                   case .pending, .inProgess(_):
                       guard let cell = self.downloadTableView.cellForRow(at: IndexPath(row: index, section: 0)) as? ProgressCell else {
                           return
                       }
                       cell.configure(task)
                       self.downloadTableView.beginUpdates()
                       self.downloadTableView.endUpdates()
                   }
               }
           })
       })
       
}

Starting the Task Operation into DispatchGroup with DispatchSemaphore

Next, we will start the downloading of task by assigning the job into the DispatchQueue and DispatchGroup. Inside the startOperation method, we will enumerate all the tasks and invoke the startTask method passing the dispatchGroup, dispatchQueue, and dispatchSemaphore. We also pass the randomizeTimer from the option to simulate random download time.

In the DownloadTask startTask method, we invoke the dispatchQueue async method passing the dispatch group. In the execution closure, here are the things we will do:

  1. Invoke the group enter method to indicates that our task execution has entered the group. We also need to balance this with the invoke of leave method when the execution of our task has finished.
  2. We also trigger the semaphore wait method to decrement the semaphore count. This also needs to be balanced with semaphore signal method when the task has finished to increment the semaphore count so it can execute another task.
  3. Between those calls, we will perform a simulation of the download by sleeping the thread in specific duration, then update the state inProgress with the increased progress count (0–100) until it is set to complete.
  4. Whenever the state is updated, a Swift property observer will invoke the task update handler closure passing the task.
@objc func startOperation() {
      // ...
      downloadTasks.forEach {
          $0.startTask(queue: dispatchQueue, group: dispatchGroup, semaphore: dispatchSemaphore, randomizeTime: self.option.isRandomizedTime)
      }
}
class DownloadTask {
    
    var progress: Int = 0
    let identifier: String
    let stateUpdateHandler: (DownloadTask) -> ()
    var state = TaskState.pending {
        didSet {
            self.stateUpdateHandler(self)
        }
    }
    
    init(identifier: String, stateUpdateHandler: @escaping (DownloadTask) -> ()) {
        self.identifier = identifier
        self.stateUpdateHandler = stateUpdateHandler
    }
    
    func startTask(queue: DispatchQueue, group: DispatchGroup, semaphore: DispatchSemaphore, randomizeTime: Bool = true) {
        queue.async(group: group) { [weak self] in
            group.enter()
            semaphore.wait()
            self?.state = .inProgess(5)
            self?.startSleep(randomizeTime: randomizeTime)
            self?.state = .inProgess(20)
            self?.startSleep(randomizeTime: randomizeTime)
            self?.state = .inProgess(40)
            self?.startSleep(randomizeTime: randomizeTime)
            self?.state = .inProgess(60)
            self?.startSleep(randomizeTime: randomizeTime)
            self?.state = .inProgess(80)
            self?.startSleep(randomizeTime: randomizeTime)
            self?.state = .completed
            group.leave()
            semaphore.signal()
        }
    }
    
    private func startSleep(randomizeTime: Bool = true) {
        Thread.sleep(forTimeInterval: randomizeTime ? Double(Int.random(in: 1...3)) : 1.0)

    }
}

Using DispatchGroup notify to receive notification of completed jobs

At last, to receive the signal when all the background download tasks has been completed, we need to invoke the group notify method passing the queue and the closure that will be invoked when all the download has been finished.

Inside the closure, we just invoke the present alert method passing the completion message. Finally, we need to make sure to enable all the button, sliders, and switch user interaction property back.

@objc func startOperation() {
    // ...
    dispatchGroup.notify(queue: .main) { [unowned self] in
        self.presentAlertWith(title: "Info", message: "All Download tasks has been completed 😋😋😋")
        self.navigationItem.rightBarButtonItem?.isEnabled = true
        self.randomizeTimeSwitch.isEnabled = true
        self.tasksCountSlider.isEnabled = true
        self.maxAsyncTasksSlider.isEnabled = true
    }
}

Try to build and run the project. Play with all the sliders and switch to see the different behaviors of the app based on the number of tasks, simultaneous running task, and simulated download timer.

You can download the completed project GitHub repository from the link at alfianlosari/GCDGroup-Complete

Conclusion

That’s it! As the next release evolution of Swift to utilize async await to perform asynchronous job, GCD still provides the best performance for us when we want to write an asynchronous job in the background. With DispatchGroup and DispatchSemaphore we can group several tasks together, perform the job in queue we want, and get notification when all the tasks has been completed.

Apple also gives us an option to use higher level abstraction using OperationQueue to perform async tasks. It has several advantages such as suspending, adding dependencies between tasks. Let’s keep the lifelong learning and keep on building great things with Swift 😋!