一.简单介绍
GCD 提供了简单的API创建串行和并行队列去执行后台任务,而不用开发者去管理线程
GCD将用于计算的线程的分配抽象到调度队列中。开发者只需要创建他们自己的调度队列,或者他们可以使用Apple提供的内置全局调度队列,其中包含了几个内置的Quality of Service (QoS)
,包括interactive
, user initiated
, utility
, and background
. GCD将自动处理线程池中的线程分配。
- DispatchGroup
在某些情况下,作为开发人员需要在后台批处理异步任务,然后在将来所有任务完成时收到通知。苹果提供了DispatchGroup类来执行这种操作。
以下是苹果对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允许聚合一组任务同步,你可以把work items添加到group,并安排它们在相同或不同队列上异步执行,当所有的work items 都执行完毕,group会执行completion handler。你也可以同步地等待组中的所有任务完成执行
DispatchGroup
也可以用来同步等待所有任务完成执行,但在本教程中我们不会这样做。
- Semaphore
通过使用传统的计数信号量来控制跨多个执行上下文对资源的访问的对象
下面是开发者可能会遇到的几个场景
-
多个网络请求需要等待其他请求完成才能继续
-
在后台执行多个视频/图像处理任务
-
需要在后台同时处理下载或上传多个文件
二. 我们将要做什么
我们将通过创建一个简单的项目来模拟后台同步下载,探索如何利用DispatchGroup和DispatchSemaphore,当所有下载任务成功完成时,我们将在UI中显示成功的对话框。它还具有多种功能,例如:
- 设置下载任务总数
- 随机分配每个任务的下载时间
- 设置可以同时运行一个队列的并发任务数
三. 初始项目
从 这里 github 下载初始项目
初始工程已经创建好了对应的UI界面,所以我们可以专注在如何使用 dispatch group & dispatch semaphore
.
我们将使用dispatch group
模拟在后台下载多个文件,同时使用dispatch semaphores
模拟同时下载的文件数量限制为指定的个数
四. 下载任务
DownloadTask
类用来模拟在后台下载一个文件的任务,类的组成:
-
一个 TaskState 枚举属性,用来管理下载任务的状态,初始值是
pending
待下载enum TaskState { case pending case inProgress(Int) case completed } 复制代码
-
一个初始化方法 接受 identifier
和 一个状态更新闭包回调 参数
/// identifier 任务标识符 用于区分其他任务 /// stateUpdateHandler 闭包回调 用于随时更新任务状态 init(identifier: String, stateUpdateHandler: @escaping (DownloadTask) -> ()) 复制代码
-
progress
变量用来表示当前下载的完成进度,当下载任务开始时,它将定期更新 -
startTask
方法暂时为空,我们稍后将会添加在DispatchGroup
和semaphore
中执行任务的代码 -
startSleep
方法将会用于将线程休眠一段指定的时间,以模拟下载一个文件
五.View Controller介绍
JobListViewController
包含两个table view
,以及几个sliders
var downloadTableView: UItableView //展示下载任务
var completedTableView: UITableView //展示已完成任务
var tasksCountSlider: UISlider // 设置下载任务数量
var maxAsyncTaskSlider: UISlide //设置可同时下载数量
var randomizeTimeSwitch: UISwitch //是否取随机时间 开启的话是随机1~3秒,否则默认1秒
复制代码
类的具体组成:
-
downloadTasks
数组用来存储所有下载的任务,顶部table view
用来展示当前下载的任务DownloadTasks
数组用来存储所有下载完成的任务,底部table view
用来展示下载完成的任务
var downlaodTasks = [DownloadTask][] { didSet { downloadTableView.reloadData() }}
var completedTasks = [DownloadTask][] { didSet { completedTableView.reloadData() }}
复制代码
-
SimulationOption
结构体,用来存储下载配置struct SimulationOption { var jobCount: Int //下载任务数 var maxAsyncTasks: Int //最大同时下载数 var isRandomizedTime: Bool //是否开启随机下载时间 } 复制代码
-
TableViewDataSource
cellForRowAtIndexPath
方法里重用progressCell
,通过传递DownloadTask
去配置不同状态的cell
-
tasksCountSlider
决定我们要在dispatch group
模拟的任务数量 -
maxAsyncTasksSlider
决定在dispatch group
同时下载任务的最大数量比如,有100个下载任务,我们只希望队列里只能同时下载10个,这时候就可以用
DispatchSemaphore
去限制这个最大值 -
randomizeTimeSwitch
是否取随机时间
六. 创建 DispatchQueue, DispatchGroup, & DispatchSemaphore
现在开始模拟当用户点击start
按钮操作,该操作会触发当前为空的startOperation
方法,
用DispatchQueue, DispatchGroup, DispatchSemaphore
各自的类创建三个变量
DispatchQueue
初始化给定一个唯一标识符,通常用反向的域名表示( reverse domain dns)
然后设置attributes
为concurrent
,这样才能异步并行多个任务。
DispatchSemaphore
初始化设置value
为maximumAsyncTaskCount
数值,来限制同时下载的任务数量
最后,当点击start
按钮后,所有的交互,包括按钮,滑杆,开关都设置成不可点击
@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)
}
复制代码
七. 创建下载任务 处理状态更新
下一步,我们根据option
属性的maximumJob
的数值来创建对应数量的任务。
给定一个标识符来初始化DownloadTask
,然后在任务状态更新的闭包里,传递callback
callback
具体实现如下
- 根据任务的标识符,从
downloadTask
数组中查找到任务对应的 index completed
状态,我们只需要将任务从downloadTasks
中移除,然后将任务插入到completedTasks
数组的index为0的位置,downloadTasks
和completedTasks
都有个属性观察,一旦变更,各自的tabe view
会触发reloadData
inProgress
状态,在downloadTableView
里通过cellForIndexPath:
方法,找到对应的ProgressCell
,调用configure
方法,传递新的状态,最终,我们会调用tableView
的beginUpdates
endUpdates
方法以防cell的高度的变化
@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()
}
}
})
}
)
}
复制代码
八. 在DispatchGroup
配合DispatchSemaphore
中开启任务
接下来,我们将任务配到DispatchQueue
和DispatchGroup
中, 开始下载任务。在startOperation
方法里,
我们会遍历所有的tasks
,调用每个task
的startTask
方法,并把dispatchGroup,dispatchQueue,dispatchSemaphore
作为参数传过去,同时把option
里的randomizeTimer
传过去模拟随机下载时间
在DownloadTask startTask
方法里,传递dispatch group
到dispatchQueue async
方法里,在闭包里我们将做如下:
- 调用
group
的enter
方法以表示我们的任务执行已进入改group
,当任务结束时,还需要调用leave
方法 - 我们还需要触发
semaphore
的wait
方法,用来减少信号量计数。当任务结束时,还需要调用semaphore
的signal
方法来增加信号量计数以便它可以执行其它任务 - 在前面方法的调用中间,我们通过在特定时间
sleeping the thread
休眠线程来模拟一个下载任务,然后增加进度计数(0-100) 来更新进度inProress
,直到将其设置为complete
- 每当状态更新时,Swift的属性观察器都会调用
task update handler
闭包 并传递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: Stirng
let stateUpdateHandler: (DownloadTask) -> ()
var state = TaskState.pending {
didSet {
//状态改变 通过回调 更新下载数组,已下载数组 tableView 和 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()
//这个用来控制同时下载的任务数量,任务结束时记得调用signal()
semaphore.wait()
//模拟下载过程
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)
//下载完成
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)
}
}
复制代码
九.使用DispatchGroup notify
接收所有任务完成通知
最后,当所有任务都完成时,可以通过group notify
方法接受到通知,我们需要传递一个queue
队列,并且有一个回调,可以在回调里处理一些任务完成后需要做的事
在回调里,我们只需要弹出一个完成消息即可,并确保所有的按钮,滑杆,开关都还原为可点击
@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
}
}
复制代码
尝试运行项目,看看在不同数量的下载任务以及不同数量的同时运行任务,和模拟的下载时间下,app的表现
十.总结
Swift的下一个版本用async aswit
来执行异步操作。但当我们想在后台执行异步操作时,GCD
仍然为我们提供了最佳性能。使用DispatchGroup
和DipatchSemaphore
,我们可以将多个任务组合在一起,在所需的队列中执行任务,并在所有任务完成后获得通知。
Apple还提供了更高级别、抽象的OperationQueue
来执行异步任务,它具有几个有点,例如暂停,添加任务之间的依赖关系。您可以从这里学习到更多
让我们继续终身学习,并继续使用Swift打造美好的事物?!