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

为何URLSession.AsyncBytes的Task取消后下载仍持续?

问题:async/await 下 URLSession.bytes(from:) 无法有效取消下载任务?

核心原因

你遇到的问题本质是:URLSession.shared.bytes(from:) 返回的 AsyncBytes 与外层 Task 的取消机制没有强绑定。

当你取消外层 Task 时,AsyncBytes 的 for-await-in 循环不会立刻终止——它会先把已经缓存到本地缓冲区的字节全部处理完毕,才会响应取消信号抛出 CancellationError。更关键的是,底层的URLSession下载任务不会因为外层Task取消而自动停止,还会继续接收数据,直到缓冲区填满或者任务本身完成。

可行解决方案

方案1:手动绑定 Task 取消与 URLSessionTask 取消

放弃直接使用 shared 的 bytes(from:),手动创建 URLSessionDataTask,并在外层 Task 取消时主动调用任务的 cancel() 方法,同时跟踪字节传输进度:

func downloadWithCancelControl(url: URL) async throws -> (progress: AsyncStream<Int64>, cancel: () -> Void) {
    let session = URLSession.shared
    var dataTask: URLSessionDataTask?
    var totalReceivedBytes: Int64 = 0
    
    // 创建用于传递进度的AsyncStream
    let progressStream = AsyncStream<Int64> { continuation in
        dataTask = session.dataTask(with: url) { data, _, error in
            if let error = error {
                continuation.finish(throwing: error)
                return
            }
            continuation.finish()
        }
        
        dataTask?.resume()
        
        // 监听Task取消
        Task {
            await Task.sleep(UInt64(0.1 * 1_000_000_000)) // 短暂延迟确保任务启动
            await Task.currentTask!.cancellation()
            dataTask?.cancel()
            continuation.finish()
        }
    }
    
    // 补充:如需实时跟踪字节,可结合URLSessionDelegate的data接收方法累加字节数并发送到stream
    let cancel = { dataTask?.cancel() }
    return (progressStream, cancel)
}

方案2:使用 async/await 版 URLSessionDownloadTask

苹果提供了原生async版的下载任务API,URLSession.shared.download(from:),它的取消机制与外层Task强绑定,取消后会立刻终止底层下载。同时可以通过自定义URLSessionDownloadDelegate跟踪已传输字节数:

class DownloadProgressDelegate: NSObject, URLSessionDownloadDelegate {
    var updateProgress: ((Int64, Int64) -> Void)?
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        updateProgress?(totalBytesWritten, totalBytesExpectedToWrite)
    }
}

func startTrackableDownload(url: URL) async throws -> (savedURL: URL, cancel: () -> Void) {
    let progressDelegate = DownloadProgressDelegate()
    let session = URLSession(configuration: .default, delegate: progressDelegate, delegateQueue: .global())
    
    // 实时打印进度
    progressDelegate.updateProgress = { written, total in
        print("已传输:\(written)/\(total) 字节")
    }
    
    let downloadTask = Task {
        let (tempURL, response) = try await session.download(from: url)
        // 将临时文件移动到持久化目录
        let destURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            .appendingPathComponent(response.suggestedFilename ?? "downloaded_file")
        try? FileManager.default.removeItem(at: destURL)
        try FileManager.default.moveItem(at: tempURL, to: destURL)
        return destURL
    }
    
    // 取消操作
    let cancelAction = {
        downloadTask.cancel()
        session.invalidateAndCancel()
    }
    
    // 监听取消,确保session资源清理
    Task {
        await downloadTask.cancellation()
        session.invalidateAndCancel()
    }
    
    let resultURL = try await downloadTask.value
    return (resultURL, cancelAction)
}

总结

URLSession.shared.bytes(from:) 更适合处理小文件或不需要精确取消控制的场景,因为它的取消响应存在延迟且无法直接干预底层任务。如果需要严格的取消控制和进度跟踪,优先选择手动绑定任务取消,或者使用async版的downloadTask结合自定义delegate方案。

内容的提问来源于stack exchange,提问作者Thecafremo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 22:55:18