使用遗留的wait
函数(无论是分派组还是信号量或您拥有的任何东西)是一种反模式.如果从主线程调用,问题可能从UI中的故障到应用程序的灾难性终止.即使从后台线程调用,它也不是一个好主意,因为它会阻塞辅助线程池中数量非常有限的线程之一,这既效率低下,而且如果耗尽该池,可能会导致更严重的问题.此外,在异步上下文中不允许wait
,因为SWIFT并发性依赖于必须允许所有线程"向前进展"(即,从不阻塞)的合同.
不幸的是,错误消息中提到的"使用一个任务组代替"有点误导.他们假设,如果您使用的是一个调度组,您正在使用它的预期目的,即管理group的任务,TaskGroup
是现代的替代方案.但你没有一组任务,而是只有一个任务.
因此,我们不会使用任务组.相反,我们将简单地将遗留异步API包装在一个withCheckedThrowingContinuation
:
extension PHImageManager {
func image(
for asset: PHAsset,
targetSize: CGSize,
contentMode: PHImageContentMode = .default,
options: PHImageRequestOptions? = nil
) async throws -> UIImage {
assert(!(options?.isSynchronous ?? false), "Synchronous image retrieval not permitted from Swift concurrency")
return try await withCheckedThrowingContinuation { continuation in
requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, _ in
if let image {
continuation.resume(returning: image)
} else {
continuation.resume(throwing: (info?[PHImageErrorKey] as? Error) ?? ImageManagerError.noImage)
}
}
}
}
}
extension PHImageManager {
enum ImageManagerError: Error {
case noImage
}
}
然后你可以这样做:
func fetch(asset: PHAsset, imageSize: CGSize) async throws -> Image {
let uiImage = try await imageManager.image(for: asset, targetSize: imageSize)
return Image(uiImage: uiImage)
}
通过遵循async
—await
模式,我们避免调用遗留的wait
API,从而避免阻塞线程.
虽然我试图保留你的例子的简单性,但有两个警告:
就像你最初的例子一样,上面的内容不处理取消.但是在编写Swift并发代码时,如果底层API支持取消(例如requestImage
),我们总是希望支持取消.
您可以修改上面的代码以处理取消,方法是将其包含在withTaskCancellationHandler
中:
extension PHImageManager {
func image(
for asset: PHAsset,
targetSize: CGSize,
contentMode: PHImageContentMode = .default,
options: PHImageRequestOptions? = nil
) async throws -> UIImage {
assert(!(options?.isSynchronous ?? false), "Synchronous image retrieval not permitted from Swift concurrency")
let request = ImageRequest(manager: self)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
guard !request.isCancelled else {
continuation.resume(throwing: CancellationError())
return
}
request.id = requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, _ in
if let image {
continuation.resume(returning: image)
} else {
continuation.resume(throwing: (info?[PHImageErrorKey] as? Error) ?? ImageManagerError.noImage)
}
}
}
} onCancel: {
request.cancel()
}
}
}
private extension PHImageManager {
class ImageRequest: @unchecked Sendable {
private weak var manager: PHImageManager?
private let lock = NSLock()
private var _id: PHImageRequestID?
private var _isCancelled = false
init(manager: PHImageManager) {
self.manager = manager
}
var id: PHImageRequestID? {
get { lock.withLock { _id } }
set { lock.withLock { _id = newValue } }
}
var isCancelled: Bool {
get { lock.withLock { _isCancelled } }
}
func cancel() {
lock.withLock {
_isCancelled = true
if let id = _id {
manager?.cancelImageRequest(id)
}
}
}
}
}
有时requestImage
会多次调用它的完成处理程序闭包(除非你使用highQualityFormat
或fastFormat
中的deliveryMode
).
就像您的调度组示例一样,withCheckedContinuation
要求您resume
只执行一次且仅执行一次.如果我们想支持多个图像(例如,本地低质量图像的检索和远程高质量图像的后续检索),我们将使用AsyncSequence
,即AsyncStream
:
extension PHImageManager {
func images(
for asset: PHAsset,
targetSize: CGSize,
contentMode: PHImageContentMode = .default,
options: PHImageRequestOptions? = nil
) -> AsyncThrowingStream<UIImage, Error> {
assert(!(options?.isSynchronous ?? false), "Synchronous image retrieval not permitted from Swift concurrency")
let request = ImageRequest(manager: self)
return AsyncThrowingStream { continuation in
request.id = requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, info in
guard let image else {
continuation.finish(throwing: (info?[PHImageErrorKey] as? Error) ?? ImageManagerError.noImage)
return
}
continuation.yield(image)
// don't finish, yet, if current result is degraded (and we didn't ask for `fastFormat`)
if
let isDegraded = info?[PHImageResultIsDegradedKey] as? Bool,
isDegraded,
options?.deliveryMode != .fastFormat
{
return
}
// otherwise, go ahead and finish
continuation.finish()
}
continuation.onTermination = { reason in
guard case .cancelled = reason else { return }
request.cancel()
}
}
}
}
然后你会做这样的事情:
func fetchImages(for asset: PHAsset, imageSize: CGSize) async throws {
for try await uiImage in imageManager.images(for: asset, targetSize: imageSize) {
let image = Image(uiImage: uiImage)
// do something with `image`
}
}
我必须承认,我发现上面的"完成了吗"逻辑有点脆弱.(Apple怎么能不提供一个简单的属性来判断请求是否完成以及完成处理程序不会再次被调用?)但是,你明白了.
我承认,我只是快速地把它放在一起,并没有做详尽的测试,但是,希望它能说明一些与在Swift并发模式中包装遗留异步API以及避免调用遗留wait
函数相关的概念.