
一.简单介绍
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 //是否开启随机下载时间 } 复制代码 -
TableViewDataSourcecellForRowAtIndexPath方法里重用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会触发reloadDatainProgress状态,在downloadTableView里通过cellForIndexPath:方法,找到对应的ProgressCell,调用configure方法,传递新的状态,最终,我们会调用tableView的beginUpdatesendUpdates方法以防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打造美好的事物?!























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)