如何在URLSession代理方法中使用DispatchGroup实现批量分批下载?
解决批量下载的并发控制与DispatchGroup死锁问题
首先,你的现有实现确实存在死锁风险,这也是导致wait()永久阻塞的核心原因,我来帮你拆解问题并给出可行的解决方案:
问题分析
- 队列死锁:你在
downloadAllDispatchQueue队列里调用了downloadAllDispatchGroup.wait(),同时又把leave()的操作通过async提交到同一个队列。如果这个队列是串行队列,wait()会直接阻塞队列的执行,导致leave()的任务永远无法被调度,最终wait()永远等不到所有leave()调用,陷入永久阻塞。 - 并发数重置逻辑错误:当
concurrentDownloads >= 10时直接把concurrentDownloads置为0是不合理的——wait()会等到当前所有已enter()的任务都leave(),这时候实际完成的任务数是10,而不是随便一个任务完成就会触发wait()结束,所以这个重置逻辑不符合你的“一批10个完成再启动下一批”的需求。
修正方案(两种可选)
方案一:用DispatchSemaphore实现更直观的并发控制
相比DispatchGroup,Semaphore天生适合这种“限制同时执行任务数”的场景,代码逻辑更清晰:
self.downloadAllDispatchQueue.async { let semaphore = DispatchSemaphore(value: 10) for i in 0..<files.count { if self.cancelDownloadAll { return } // 等待信号量,直到有空闲位置 semaphore.wait() guard let workVariantPart = libraryWorkVariantParts[i].workVariantPart else { semaphore.signal() // 即使没有可下载的内容,也要释放信号量 continue } // 启动下载任务,在任务完成(成功/失败)时释放信号量 self.startDownload(for: workVariantPart) { [weak self] in semaphore.signal() // 这里可以处理单个任务完成后的逻辑,比如更新进度 } } DispatchQueue.main.async { // 所有下载任务都已提交完成,若需等待最后一批结束可额外处理 } }
方案二:修复DispatchGroup的实现(避免死锁)
如果你坚持使用DispatchGroup,需要解决队列死锁问题,并修正并发数控制逻辑:
self.downloadAllDispatchQueue.async { var downloadGroup = DispatchGroup() let maximumConcurrentDownloads = 10 var activeDownloads = 0 for i in 0..<files.count { if self.cancelDownloadAll { return } // 当活跃任务数达到上限时,等待当前组内任务全部完成 if activeDownloads >= maximumConcurrentDownloads { downloadGroup.wait() activeDownloads = 0 // 重置Group,因为上一批任务已经全部完成 downloadGroup = DispatchGroup() } guard let workVariantPart = libraryWorkVariantParts[i].workVariantPart else { continue } activeDownloads += 1 downloadGroup.enter() // 启动下载任务 self.startDownload(for: workVariantPart) { [weak self] in // 直接在任务完成回调里调用leave,不要提交到阻塞的队列 downloadGroup.leave() // 处理单个任务完成逻辑 } } // 等待最后一批任务完成 downloadGroup.wait() DispatchQueue.main.async { // 所有下载完成 } } // 代理方法简化(如果用闭包回调的话,甚至可以不用代理) func downloadDidFinish(_ notification: Notification) { // 直接找到对应的任务Group调用leave,注意要保证线程安全 if let downloadGroup = self.downloadAllDispatchGroup { // 不要提交到阻塞的downloadAllDispatchQueue,直接调用 downloadGroup.leave() } }
关键注意事项
- 避免在串行队列中阻塞后再提交任务到该队列:这是死锁的常见诱因,
leave()操作要么直接在任务完成的线程调用(如果线程安全),要么提交到全局队列或其他非阻塞的队列。 - enter和leave必须严格配对:每个
enter()必须对应一个leave(),即使下载失败也要调用leave(),否则Group会一直等待。 - 弱引用避免循环引用:在闭包或代理回调中使用
[weak self],防止内存泄漏。
内容的提问来源于stack exchange,提问作者Koushik Ravikumar
相关产品推荐
相关产品推荐

