Translation [actual combat]: download tasks using GCD Group and Semaphore

Translation [actual combat]: download tasks using GCD Group and Semaphore

original

1. Brief introduction

GCD provides a simple API to create serial and parallel queues to perform background tasks without the developer to manage threads

GCD abstracts the allocation of threads for calculation into a dispatch queue. Developers only need to create their own dispatch queue, or they can use the built-in global dispatch queue provided by Apple, which contains several built-in

Quality of Service (QoS)
,include
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 when all tasks are completed in the future. Apple provides the DispatchGroup class to perform this operation.

The following is an Apple pair

DispatchGroup
A brief summary.

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 a group of tasks to be aggregated synchronously. You can add work items to the group and arrange them to be executed asynchronously on the same or different queues. When all the work items are executed, the group will execute the completion handler. You can also wait for all tasks in the group to complete execution synchronously

DispatchGroup
It can also be used to synchronously wait for all tasks to complete execution, but we will not do this in this tutorial.

  • Semaphore

Objects that control access to resources across multiple execution contexts by using traditional counting semaphores

Here are a few scenarios that developers may 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. Need to process download or upload multiple files at the same time in the background

2. What are we going to do

We will create a simple project to simulate the background synchronization download, and explore how to use DispatchGroup and DispatchSemaphore. When all download tasks are successfully completed, we will display a successful dialog box in the UI. It also has a variety of functions, such as:

  1. Set the total number of download tasks
  2. Randomly allocate download time for each task
  3. Set the number of concurrent tasks that can run a queue at the same time

3. Initial project

Download the initial project from here github

The initial project has already created the corresponding UI interface, so we can focus on how to use it

dispatch group & dispatch semaphore
.

We will use

dispatch group
Simulate downloading multiple files in the background and use them at the same time
dispatch semaphores
Simulate the number of files downloaded at the same time is limited to the specified number

4. Download task

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

  1. A TaskState enumeration property, used to manage the state of the download task, the initial value is

    pending
    To be downloaded

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

    And a status update closure callback parameter

    ///identifier task identifier is used to distinguish other tasks ///stateUpdateHandler closure callback is used to update task state at any time init ( identifier : String , stateUpdateHandler : @escaping ( DownloadTask ) -> ()) copying the code
  3. progress
    Variables are used to indicate the completion progress of the current download. When the download task starts, it will be updated regularly

  4. startTask
    The method is temporarily empty, we will add it later
    DispatchGroup
    with
    semaphore
    Code to perform tasks in

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

V. Introduction to View Controller

JobListViewController
Contains two
table view
, And several
sliders

var downloadTableView: UItableView //Display download tasks 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 //Whether to take a random time open then random 1 to 3 seconds, otherwise the default one second to copy the code

The specific composition of the class:

  1. downloadTasks
    The array is used to store all downloaded tasks, the top
    table view
    Used to display the currently downloaded task

    DownloadTasks
    The array is used to store all downloaded tasks, the bottom
    table view
    Used to show the completed tasks of the download

var downlaodTasks = [ DownloadTask ] [] { didSet {downloadTableView.reloadData ()}} var completedTasks = [ DownloadTask ] [] { didSet {completedTableView.reloadData ()}} Copy the code
  1. SimulationOption
    Structure, used to store download configuration

    struct SimulationOption { var jobCount: Int //Number of download tasks var maxAsyncTasks: Int //Maximum number of simultaneous downloads var isRandomizedTime: Bool //Whether to enable random download time } Copy code
  2. TableViewDataSource
    cellForRowAtIndexPath
    Reuse in method
    progressCell
    , By passing
    DownloadTask
    To configure different states
    cell

  3. tasksCountSlider
    Decided we want to be in
    dispatch group
    Number of simulated tasks

  4. maxAsyncTasksSlider
    Decided on
    dispatch group
    Maximum number of simultaneous download tasks

    For example, if there are 100 download tasks, we only hope that there can only be 10 downloads in the queue at the same time, then it can be used

    DispatchSemaphore
    To limit this maximum

  5. randomizeTimeSwitch
    Whether to take random time

6. Create DispatchQueue, DispatchGroup, & DispatchSemaphore

Now start to simulate when the user clicks

start
Button operation, this operation will trigger the currently empty
startOperation
method,

use

DispatchQueue, DispatchGroup, DispatchSemaphore
Create three variables for each class

DispatchQueue
Initialize given a unique identifier, usually represented by reverse domain dns (reverse domain dns)

Then set

attributes
for
concurrent
, So that multiple tasks can be asynchronously paralleled.

DispatchSemaphore
Initialize settings
value
for
maximumAsyncTaskCount
Value to limit the number of tasks that can be downloaded at the same time

Finally, when you click

start
After the button, all interactions, including buttons, sliders, and switches, are set to be non-clickable

@objc func startOperation () { 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 () let dispatchSemaphore = DispatchSemaphore (value: option.maxAsyncTasks) } Copy code

7. Create download task processing status update

Next, we based

option
Attributive
maximumJob
To create the corresponding number of tasks.

Given an identifier to initialize

DownloadTask
, And then pass in the closure of task status update
callback

callback
The specific implementation is as follows

  1. According to the task s identifier, from
    downloadTask
    Find the index corresponding to the task in the array
  2. completed
    Status, we only need to change the task from
    downloadTasks
    Remove it, and then insert the task into
    completedTasks
    The position where the index of the array is 0,
    downloadTasks
    with
    completedTasks
    All have an attribute observation, once changed, their respective
    tabe view
    Will trigger
    reloadData
  3. inProgress
    Status in
    downloadTableView
    Pass in
    cellForIndexPath:
    Method to find the corresponding
    ProgressCell
    ,transfer
    configure
    Method, pass the new state, and eventually, we will call
    tableView
    of
    beginUpdates
    endUpdates
    Method to prevent the height of the cell from changing
@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.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 code

8. In
DispatchGroup
Cooperate
DispatchSemaphore
Open task in

Next, we assign the task to

DispatchQueue
with
DispatchGroup
In, start the download task. in
startOperation
In the method,

We will iterate through all

tasks
, Call each
task
of
startTask
Method and put
dispatchGroup, dispatchQueue, dispatchSemaphore
Passed as a parameter, and at the same time
option
inner
randomizeTimer
Pass in the past to simulate random download time

in

DownloadTask startTask
In the method, pass
dispatch group
To
dispatchQueue async
In the method, in the closure we will do the following:

  1. transfer
    group
    of
    enter
    Method to indicate that our task execution has entered the reform
    group
    , When the task is over, you also need to call
    leave
    method
  2. We also need to trigger
    semaphore
    of
    wait
    Method to reduce the semaphore count. When the task is over, you also need to call
    semaphore
    of
    signal
    Method to increase the semaphore count so that it can perform other tasks
  3. In the middle of the call of the previous method, we pass at a specific time
    sleeping the thread
    Sleep the thread to simulate a download task, and then increase the progress count (0-100) to update the progress
    inProress
    Until it is set to
    complete
  4. Whenever the state is updated, Swift's property observer will call
    task update handler
    Closure and pass
    task
@objc func startOperation () { //... downloadTasks.forEach { $0 .startTask(queue: dispatchQueue, group: dispatchGroup, semaphore: dispatchSemaphore, randomizeTime: self .option.isRandomizedTime) } } Copy code
class DownloadTask { var progress: Int = 0 let identifier: Stirng let stateUpdateHandler: ( DownloadTask ) -> () var state = TaskState .pending { didSet { //State change updates the download array through callbacks, the downloaded 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 is used to control the number of tasks downloaded at the same time, remember to call signal() when the task ends 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 code

Nine. Use
DispatchGroup notify
Receive notifications of completion of all tasks

Finally, when all tasks are completed, you can pass

group notify
Method receives the notification, we need to pass a
queue
Queue, and there is a callback, you can handle some tasks that need to be done in the callback

In the callback, we only need to pop up a completion message, and ensure that all buttons, sliders, and switches are restored to be clickable

@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 } } Copy code

Try to run the project to see how the app performs under different numbers of download tasks, different numbers of simultaneous tasks, and simulated download times

You can download the complete project code here

X. Summary

The next version of Swift uses

async aswit
To perform asynchronous operations. But when we want to perform asynchronous operations in the background,
GCD
Still provides us with the best performance. use
DispatchGroup
with
DipatchSemaphore
, We can group multiple tasks together, execute tasks in the required queue, and get notified when all tasks are completed.

Apple also provides a higher level, abstract

OperationQueue
To perform asynchronous tasks, it has several advantages, such as pausing and adding dependencies between tasks. You can learn more from here

Let us continue to learn for life and continue to use Swift to create beautiful things !