Swift iOS:如何取消DispatchGroup管理的循环任务
嘿,我看你碰到了DispatchGroup在出错场景下卡着没法结束的问题——毕竟每次调用enter()后必须对应一个leave(),不然整个group会一直处于等待状态,不仅触发不了后续的notify,还可能造成资源浪费。下面给你几个实用的解决思路,结合你的代码来调整:
方案1:强制平衡enter()/leave() + 全局错误标记
DispatchGroup本身没有“取消”API,最直接的解决方式是确保每个enter()都有对应的leave(),不管任务成功还是失败。同时用一个全局变量标记是否发生错误,在notify回调里判断是否执行后续的数据库上传。
修改后的关键代码片段:
首先添加一个错误标记变量:
var hasError = false // 新增:标记是否发生错误
然后在每个错误触发点补充leave(),并标记错误:
// 1. URLSession错误处理 if error != nil { self.hasError = true self.showAlert() self.myGroup.leave() // 必须调用,平衡之前的enter() return } // 2. Storage putData错误处理 if error != nil{ self.hasError = true self.showAlert() self.myGroup.leave() return } // 3. Storage downloadURL错误处理 if error != nil{ self.hasError = true self.showAlert() self.myGroup.leave() return } // 4. downloadURL为nil的错误处理 else{ self.hasError = true self.showAlert() self.myGroup.leave() // 补充leave() }
最后在notify回调里判断错误状态:
myGroup.notify(queue: .global(qos: .background)) { DispatchQueue.main.async { // 切回主线程处理状态和UI if !self.hasError { self.sendDataFromDictToFirebaseDatabase() } self.count = 0 self.session.invalidateAndCancel() } }
方案2:结合DispatchWorkItem管理可取消任务
如果需要更精细地控制未执行的任务,可以把每个循环任务包装成DispatchWorkItem,出错时取消所有未启动的任务,同时确保已启动的任务都调用leave()。
修改后的关键代码:
首先定义数组保存WorkItem:
var workItems = [DispatchWorkItem]() // 新增:保存所有任务项
然后在循环中创建并执行WorkItem:
for url in urls{ myGroup.enter() let workItem = DispatchWorkItem { [weak self] in guard let self = self else { self?.myGroup.leave() // 避免泄漏 return } self.session.dataTask(with: url!, completionHandler: { (data, response, error) in // ... 原有的错误和成功处理逻辑(记得每个分支都要leave()) }).resume() } workItems.append(workItem) Dispatch.global().async(execute: workItem) }
最后在showAlert()里取消所有未执行的任务:
func showAlert(){ session.invalidateAndCancel() count = 0 // 取消所有未执行的任务 workItems.forEach { $0.cancel() } workItems.removeAll() // 弹出提示 let alert = UIAlertController(title: "错误", message: "操作失败,请重试", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "确定", style: .default)) present(alert, animated: true) }
方案3:用Async/Await的Task Group替代DispatchGroup(推荐iOS15+)
如果你的项目支持iOS15+/macOS12+,用Async/Await的Task Group会更简洁,错误处理更自然,完全不用手动管理enter()/leave()。
示例代码:
func processAllImages() async { var dict = [String: Any]() let picUUID = UUID().uuidString let session = URLSession.shared let storageRoot = Storage.storage().reference() await withThrowingTaskGroup(of: (String, String).self) { group in // 为每个URL添加异步任务 for (index, url) in urls.enumerated() { group.addTask { // 1. 下载数据 let (data, _) = try await session.data(from: url) // 2. 上传到Storage let picId = "\(picUUID)_\(index).jpg" let picRef = storageRoot.child("pics/\(picId)") _ = try await picRef.putDataAsync(data) // 3. 获取下载URL let downloadURL = try await picRef.downloadURL() guard let picUrlString = downloadURL.absoluteString else { throw NSError(domain: "Invalid URL", code: -1) } return (picId, picUrlString) } } // 处理任务结果或捕获错误 do { for try await (picId, picUrl) in group { dict["picId_\(index)"] = picId dict["picUrl_\(index)"] = picUrl } // 所有任务成功,上传到数据库 await sendDataFromDictToFirebaseDatabase(dict: dict) } catch { // 任意任务出错,取消所有剩余任务 group.cancelAll() DispatchQueue.main.async { self.showAlert() } } } session.invalidateAndCancel() } // 异步版本的数据库上传 func sendDataFromDictToFirebaseDatabase(dict: [String: Any]) async { // 你的数据库上传逻辑(用Async/Await版本的Firebase API) }
优势:
- Task Group自动管理任务生命周期,出错时调用
cancelAll()就能终止所有未完成的任务 - 错误处理更集中,不用在多个回调里重复写逻辑
- 代码可读性更强,避免嵌套回调地狱
额外注意事项
- 一定要确保
enter()和leave()的数量严格相等,否则会导致DispatchGroup永久等待,造成内存泄漏 - 在闭包里使用
[weak self]避免循环引用,尤其是在长时间运行的异步任务中 - 出错后要及时清理资源(比如
session.invalidateAndCancel())和重置状态(count、dict等)
内容的提问来源于stack exchange,提问作者Lance Samaria
相关产品推荐
相关产品推荐

