译文[实战]:使用GCD Group和Semaphore下载任务

原文

1_YVL1X3U938OHX_yYnXx_hQ

一.简单介绍

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

通过使用传统的计数信号量来控制跨多个执行上下文对资源的访问的对象

下面是开发者可能会遇到的几个场景

  1. 多个网络请求需要等待其他请求完成才能继续

  2. 在后台执行多个视频/图像处理任务

  3. 需要在后台同时处理下载或上传多个文件

二. 我们将要做什么

我们将通过创建一个简单的项目来模拟后台同步下载,探索如何利用DispatchGroup和DispatchSemaphore,当所有下载任务成功完成时,我们将在UI中显示成功的对话框。它还具有多种功能,例如:

  1. 设置下载任务总数
  2. 随机分配每个任务的下载时间
  3. 设置可以同时运行一个队列的并发任务数

三. 初始项目

从 这里 github 下载初始项目

初始工程已经创建好了对应的UI界面,所以我们可以专注在如何使用 dispatch group & dispatch semaphore.

我们将使用dispatch group模拟在后台下载多个文件,同时使用dispatch semaphores模拟同时下载的文件数量限制为指定的个数

四. 下载任务

DownloadTask类用来模拟在后台下载一个文件的任务,类的组成:

  1. 一个 TaskState 枚举属性,用来管理下载任务的状态,初始值是 pending 待下载

    enum TaskState {
      case pending 
      case inProgress(Int)
      case completed
    }
    复制代码
  2. 一个初始化方法 接受 identifier

    和 一个状态更新闭包回调 参数

    /// identifier 任务标识符 用于区分其他任务
    /// stateUpdateHandler 闭包回调 用于随时更新任务状态
    init(identifier: String, stateUpdateHandler: @escaping (DownloadTask) -> ())
    复制代码
  3. progress 变量用来表示当前下载的完成进度,当下载任务开始时,它将定期更新

  4. startTask 方法暂时为空,我们稍后将会添加在DispatchGroupsemaphore中执行任务的代码

  5. startSleep方法将会用于将线程休眠一段指定的时间,以模拟下载一个文件

五.View Controller介绍

1_b1_CAk9vEdy2Qs0fMtI0Ww

JobListViewController 包含两个table view,以及几个sliders

var downloadTableView: UItableView //展示下载任务
var completedTableView: UITableView //展示已完成任务
var tasksCountSlider: UISlider // 设置下载任务数量
var maxAsyncTaskSlider: UISlide //设置可同时下载数量
var randomizeTimeSwitch: UISwitch //是否取随机时间 开启的话是随机1~3秒,否则默认1秒
复制代码

类的具体组成:

  1. downloadTasks 数组用来存储所有下载的任务,顶部table view用来展示当前下载的任务

    DownloadTasks 数组用来存储所有下载完成的任务,底部table view用来展示下载完成的任务

var downlaodTasks = [DownloadTask][] { didSet { downloadTableView.reloadData() }}
var completedTasks = [DownloadTask][] { didSet { completedTableView.reloadData() }}
复制代码
  1. SimulationOption结构体,用来存储下载配置

    struct SimulationOption {
      var jobCount: Int 	        //下载任务数
      var maxAsyncTasks: Int 	//最大同时下载数
      var isRandomizedTime: Bool	//是否开启随机下载时间
    }
    复制代码
  2. TableViewDataSource cellForRowAtIndexPath 方法里重用 progressCell,通过传递DownloadTask去配置不同状态的cell

  3. tasksCountSlider 决定我们要在dispatch group模拟的任务数量

  4. maxAsyncTasksSlider 决定在dispatch group 同时下载任务的最大数量

    比如,有100个下载任务,我们只希望队列里只能同时下载10个,这时候就可以用DispatchSemaphore去限制这个最大值

  5. randomizeTimeSwitch 是否取随机时间

六. 创建 DispatchQueue, DispatchGroup, & DispatchSemaphore

现在开始模拟当用户点击start按钮操作,该操作会触发当前为空的startOperation方法,

DispatchQueue, DispatchGroup, DispatchSemaphore各自的类创建三个变量

DispatchQueue 初始化给定一个唯一标识符,通常用反向的域名表示( reverse domain dns)

然后设置attributesconcurrent,这样才能异步并行多个任务。

DispatchSemaphore 初始化设置valuemaximumAsyncTaskCount数值,来限制同时下载的任务数量

最后,当点击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具体实现如下

  1. 根据任务的标识符,从downloadTask数组中查找到任务对应的 index
  2. completed状态,我们只需要将任务从downloadTasks中移除,然后将任务插入到completedTasks数组的index为0的位置,downloadTaskscompletedTasks都有个属性观察,一旦变更,各自的tabe view会触发reloadData
  3. inProgress状态,在downloadTableView里通过cellForIndexPath:方法,找到对应的ProgressCell,调用configure方法,传递新的状态,最终,我们会调用tableViewbeginUpdates 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中开启任务

接下来,我们将任务配到DispatchQueueDispatchGroup中, 开始下载任务。在startOperation方法里,

我们会遍历所有的tasks,调用每个taskstartTask方法,并把dispatchGroup,dispatchQueue,dispatchSemaphore作为参数传过去,同时把option里的randomizeTimer传过去模拟随机下载时间

DownloadTask startTask方法里,传递dispatch groupdispatchQueue async方法里,在闭包里我们将做如下:

  1. 调用groupenter方法以表示我们的任务执行已进入改group,当任务结束时,还需要调用leave方法
  2. 我们还需要触发semaphorewait方法,用来减少信号量计数。当任务结束时,还需要调用semaphoresignal方法来增加信号量计数以便它可以执行其它任务
  3. 在前面方法的调用中间,我们通过在特定时间sleeping the thread休眠线程来模拟一个下载任务,然后增加进度计数(0-100) 来更新进度inProress,直到将其设置为complete
  4. 每当状态更新时,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仍然为我们提供了最佳性能。使用DispatchGroupDipatchSemaphore,我们可以将多个任务组合在一起,在所需的队列中执行任务,并在所有任务完成后获得通知。

Apple还提供了更高级别、抽象的OperationQueue来执行异步任务,它具有几个有点,例如暂停,添加任务之间的依赖关系。您可以从这里学习到更多

让我们继续终身学习,并继续使用Swift打造美好的事物?!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享