You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在URLSession代理方法中使用DispatchGroup实现批量分批下载?

解决批量下载的并发控制与DispatchGroup死锁问题

首先,你的现有实现确实存在死锁风险,这也是导致wait()永久阻塞的核心原因,我来帮你拆解问题并给出可行的解决方案:

问题分析

  1. 队列死锁:你在downloadAllDispatchQueue队列里调用了downloadAllDispatchGroup.wait(),同时又把leave()的操作通过async提交到同一个队列。如果这个队列是串行队列,wait()会直接阻塞队列的执行,导致leave()的任务永远无法被调度,最终wait()永远等不到所有leave()调用,陷入永久阻塞。
  2. 并发数重置逻辑错误:当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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 13:52:40