The original

A brief introduction

The GCD provides a simple API for creating serial and parallel queues to perform background tasks without the developer having to manage threads

GCD abstracts the allocation of threads for computation into a scheduling queue. Developers just need to create their own scheduling queue, or they can use the built-in global scheduling queue provided by Apple, which contains several built-in Quality of Service (QoS), Interactive, user initiated, Utility, and background. GCD will automatically handle thread allocation in the thread pool.

  • DispatchGroup

In some cases, as a developer, you need to batch asynchronous tasks in the background and then be notified in the future when all tasks are complete. Apple provides the DispatchGroup class to perform this operation.

Here’s a brief summary of apple’s DispatchGroup.

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.

Groups allow you to aggregate a set of tasks to synchronize. You can add work items to a group and schedule them to be executed asynchronously on the same or different queues. When all work items have been executed, the group executes a Completion handler. You can also wait synchronously for all tasks in the group to complete execution

DispatchGroup can also be used to wait synchronously for all tasks to complete execution, but we won’t do that in this tutorial.

  • Semaphore

An object that controls access to resources across multiple execution contexts by using traditional counting semaphores

Here are a few scenarios that developers might encounter

  1. Multiple network requests need to wait for other requests to complete before continuing

  2. Perform multiple video/image processing tasks in the background

  3. Multiple files need to be downloaded or uploaded simultaneously in the background

Two. What are we going to do

We will simulate a background synchronous download by creating a simple project, exploring how to utilize DispatchGroup and DispatchSemaphore, and displaying a success dialog in the UI when all the download tasks have completed successfully. It also has a variety of functions, such as:

  1. Set the total number of download tasks
  2. Assign each task a random download time
  3. Sets the number of concurrent tasks that can run a queue at the same time

Iii. Initial project

Download the initial project from Github here

The initial project has created the corresponding UI, so we can focus on how to use dispatch Group & Dispatch Semaphore.

We will use the Dispatch Group simulation to download multiple files in the background and use the Dispatch Semaphores simulation to limit the number of files downloaded simultaneously to a specified number

4. Download tasks

The DownloadTask class is used to simulate the task of downloading a file in the background.

  1. A TaskState enumeration property that manages the status of the download task. The initial value is Pending

    enum TaskState {
      case pending 
      case inProgress(Int)
      case completed
    }
    Copy the code
  2. An initializer method accepts an identifier

    And a status update closure callback parameter

    /// identifier The task identifier is used to distinguish other tasks
    /// the stateUpdateHandler closure callback is used to update the task status at any time
    init(identifier: String.stateUpdateHandler: @escaping (DownloadTask) - > ())
    Copy the code
  3. The progress variable is used to indicate the completion of the current download and will be updated periodically as the download task begins

  4. The startTask method is temporarily empty and we will add the code to perform tasks in DispatchGroup and Semaphore later

  5. The startSleep method will be used to hibernate the thread for a specified period of time to simulate downloading a file

V. Introduction to View Controller

The JobListViewController contains two table Views and several sliders

var downloadTableView: UItableView // Display the download task
var completedTableView: UITableView // Display completed tasks
var tasksCountSlider: UISlider // Set the number of download tasks
var maxAsyncTaskSlider: UISlide // Set the number of simultaneous downloads
var randomizeTimeSwitch: UISwitch If this parameter is enabled, the value is random for 1 to 3 seconds. If this parameter is not enabled, the value is random for 1 second
Copy the code

The specific composition of the class:

  1. The downloadTasks array is used to store all downloaded tasks, with the top table View showing the currently downloaded tasks

    The DownloadTasks array is used to store all the downloaded tasks, and the bottom table View is used to display the downloaded tasks

var downlaodTasks = [DownloadTask] [] {didSet { downloadTableView.reloadData() }}
var completedTasks = [DownloadTask] [] {didSet { completedTableView.reloadData() }}
Copy the code
  1. SimulationOption structure for storing downloaded configurations

    struct SimulationOption {
      var jobCount: Int 	        // Download the number of tasks
      var maxAsyncTasks: Int 	// Maximum number of simultaneous downloads
      var isRandomizedTime: Bool	// Whether to enable random download time
    }
    Copy the code
  2. The TableViewDataSource cellForRowAtIndexPath method reuses progressCell to configure cells in different states by passing DownloadTask

  3. The tasksCountSlider determines the number of tasks we want to emulate in the Dispatch Group

  4. MaxAsyncTasksSlider determines the maximum number of tasks to download simultaneously in the Dispatch Group

    For example, if there are 100 downloads and we only want 10 downloads in the queue at the same time, we can limit this maximum with DispatchSemaphore

  5. RandomizeTimeSwitch Whether to select a random time

Create DispatchQueue, DispatchGroup, & DispatchSemaphore

Now simulate when the user clicks the start button, which triggers the currently empty startOperation method,

Create three variables with DispatchQueue, DispatchGroup, and DispatchSemaphore classes

DispatchQueue initialization is given a unique identifier, usually represented by a reverse domain DNS.

Then set Attributes to Concurrent to asynchronously parallel multiple tasks.

DispatchSemaphore initialization sets value to maximumAsyncTaskCount to limit the number of tasks downloaded at the same time

Finally, when the Start button is clicked, all interactions, including buttons, sliders, and switches, are set to unclickable

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

Create download task processing status updates

Next, we create the corresponding number of tasks based on the value of maximumJobs in the Option property.

Initialize the DownloadTask with an identifier, and pass the callback in the task status update closure

The callback is implemented as follows

  1. Based on the task identifier, fromdownloadTaskThe index corresponding to the task is found in the array
  2. completedState, we just need to take the task fromdownloadTasks, and then inserts the task intocompletedTasksWhere the array index is 0,downloadTaskscompletedTasksEach has a property to observe, once changed, the respectivetabe viewWill triggerreloadData
  3. inProgressState,downloadTableViewIn addition to thecellForIndexPath:Method, find the correspondingProgressCell, the callconfigureMethod, pass the new state, and eventually, we’ll calltableViewthebeginUpdates endUpdatesMethod to prevent cell height changes
@objc func startOperation(a) {
  // ...
  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.completedTask.insert(task, at: 0)
                  case .pending,.inProgress(_) :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()
                }
             }                                                          
       })                                     
  }
  )
}
Copy the code

In eight.DispatchGroupCooperate withDispatchSemaphoreStart task in

Next, we assign tasks to DispatchQueue and DispatchGroup and start downloading tasks. In the startOperation method,

We will iterate over all the tasks, call each task startTask method, and the dispatchGroup dispatchQueue, dispatchSemaphore passed as a parameter, At the same time, pass the randomizeTimer in option to simulate the random download time

In the DownloadTask startTask method, passing the Dispatch Group to the dispatchQueue Async method, in the closure we’ll do the following:

  1. callgrouptheenterMethod to indicate that our task execution has changedgroupIs also called when the task endsleavemethods
  2. We also need to triggersemaphorethewaitMethod used to reduce semaphore counts. It also needs to be called when the task endssemaphorethesignalMethod to increase the semaphore count so that it can perform other tasks
  3. In the middle of the call to the previous method, we pass thesleeping the threadHibernate the thread to simulate a download task, and then increase the progress count (0-100) to update the progressinProressUntil it is set tocomplete
  4. Swift’s property viewer is called every time a status update occurstask update handlerClosure and passtask
@objc func startOperation(a) {
  // ...
  downloadTasks.forEach {
     $0.startTask(queue: dispatchQueue, group: dispatchGroup, semaphore: dispatchSemaphore, randomizeTime: self.option.isRandomizedTime)
  }
}
Copy the code
class DownloadTask {
  
  var progress: Int = 0
  let identifier: Stirng
  let stateUpdateHandler: (DownloadTask) - > ()var state = TaskState.pending {
    didSet {
      // The state changes through the callback update download array, has downloaded the array tableView and cell
      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()
        // This controls the number of tasks that can be downloaded at the same time. Call signal() when the task is finished.
        semaphore.wait()
        // Simulate the download process
        self?.state = .inProgress(5)
        self?.startSleep(randomizeTime: randomizeTime)
        self?.state = .inProgress(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)
        // Download complete
        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)}}Copy the code

Nine.DispatchGroup notifyReceive notifications of completion of all tasks

Finally, when all the tasks are complete, we can receive notification through the group notify method, we need to pass a queue, and there is a callback, in which we can handle something that needs to be done after the task is complete

In the callback, all we need to do is pop up a complete message and make sure all the buttons, sliders, and switches are clickable

@objc func startOperation(a) {
  // ...
  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}}Copy the code

Try running the project and see how the app performs with different numbers of downloads, different numbers of simultaneous runs, and simulated download times

You can download the full project code here

Ten. Summary

The next version of Swift uses Async aswit to perform asynchronous operations. But GCD still gives us the best performance when we want to perform asynchronous operations in the background. Using DispatchGroup and DipatchSemaphore, we can group multiple tasks together, execute them in the desired queue and get notified when all the tasks are complete.

Apple also provides a higher-level, abstract OperationQueue to execute asynchronous tasks, which has several advantages, such as pausing and adding dependencies between tasks. You can learn more here

Let’s continue lifelong learning and continue to use Swift to build wonderful things 😋!