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
-
Multiple network requests need to wait for other requests to complete before continuing
-
Perform multiple video/image processing tasks in the background
-
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:
- Set the total number of download tasks
- Assign each task a random download time
- 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.
-
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
-
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
-
The progress variable is used to indicate the completion of the current download and will be updated periodically as the download task begins
-
The startTask method is temporarily empty and we will add the code to perform tasks in DispatchGroup and Semaphore later
-
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:
-
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
-
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
-
The TableViewDataSource cellForRowAtIndexPath method reuses progressCell to configure cells in different states by passing DownloadTask
-
The tasksCountSlider determines the number of tasks we want to emulate in the Dispatch Group
-
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
-
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
- Based on the task identifier, from
downloadTask
The index corresponding to the task is found in the array completed
State, we just need to take the task fromdownloadTasks
, and then inserts the task intocompletedTasks
Where the array index is 0,downloadTasks
和completedTasks
Each has a property to observe, once changed, the respectivetabe view
Will triggerreloadData
inProgress
State,downloadTableView
In addition to thecellForIndexPath:
Method, find the correspondingProgressCell
, the callconfigure
Method, pass the new state, and eventually, we’ll calltableView
thebeginUpdates
endUpdates
Method 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.DispatchGroup
Cooperate withDispatchSemaphore
Start 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:
- call
group
theenter
Method to indicate that our task execution has changedgroup
Is also called when the task endsleave
methods - We also need to trigger
semaphore
thewait
Method used to reduce semaphore counts. It also needs to be called when the task endssemaphore
thesignal
Method to increase the semaphore count so that it can perform other tasks - In the middle of the call to the previous method, we pass the
sleeping the thread
Hibernate the thread to simulate a download task, and then increase the progress count (0-100) to update the progressinProress
Until it is set tocomplete
- Swift’s property viewer is called every time a status update occurs
task update handler
Closure 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 notify
Receive 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 😋!