0

如果您有一个生成值的异步方法,那么迭代该方法以生成值数组的最有效方法是什么?

protocol ImageFetching {
    var title: String { get }
    func fetch(from url: URL) async throws -> UIImage
    func fetch(from urls: [URL]) async throws -> [UIImage]
}

这个类通过使用一个遍历 url 并调用其单值方法的 TaskGroup 来生成一个数组fetch(from:) -> UIImage

class ImageService: ImageFetching {

    enum Error: Swift.Error {
        case badData
    }

    var title: String { String(describing: Self.self) }

    func fetch(from url: URL) async throws -> UIImage {
        let session = URLSession(configuration: .default)
        let data = try await session.data(from: url).0
        guard let image = UIImage(data: data) else { throw Error.badData }
        return image
    }

    func fetch(from urls: [URL]) async throws -> [UIImage] {
        return try await withThrowingTaskGroup(of: UIImage.self) { group in
            for url in urls {
                group.addTask {
                    try await self.fetch(from: url)
                }
            }

            var items: [UIImage] = []
            for try await item in group {
                items.append(item)
            }
            return items
        }
    }
}

该类不调用单值方法。相反,它URLSession.data(from:)手动UIImage(data:)调用。奇怪的是,这比ImageService上面的实现效率低。

class ImageServiceUnrolled: ImageService {

    override func fetch(from urls: [URL]) async throws -> [UIImage] {
        let session = URLSession(configuration: .default)
        return try await withThrowingTaskGroup(of: UIImage.self) { group in
            for url in urls {
                group.addTask {
                    let data = try await session.data(from: url).0
                    guard let image = UIImage(data: data) else { throw Error.badData }
                    return image
                }
            }

            var items: [UIImage] = []
            for try await item in group {
                items.append(item)
            }
            return items
        }
    }
}

此类使用AsyncStream带有展开初始化程序的 an。这是效率最低的方法。

class ImageServiceAsyncStreamUnfolding: ImageService {

    override func fetch(from urls: [URL]) async throws -> [UIImage] {
        var it = urls.makeIterator()
        return try await AsyncStream(unfolding: {
            it.next()
        })
        .map { url -> UIImage in
            try await self.fetch(from: url)
        }
        .reduce(into: [], { partialResult, image in
            partialResult.append(image)
        })
    }
}

此类使用AsyncStream带有延续初始值设定项的 an。它比上面的实现稍微高效一些。

class ImageServiceAsyncStreamContinue: ImageService {

    override func fetch(from urls: [URL]) async throws -> [UIImage] {
        return try await AsyncStream.init { continuation in
            Task.detached {
                for url in urls {
                    continuation.yield(url)
                }
                continuation.finish()
            }
        }
        .map { url -> UIImage in
            try await self.fetch(from: url)
        }
        .reduce(into: [], { partialResult, image in
            partialResult.append(image)
        })
    }
}

我很好奇为什么第一个实现是最有效的,尤其是为什么它比第二个实现更有效。至于 AsyncStream 的实现,它们似乎效率很低。

正在使用TaskGroup最好的方法来解决这个问题吗?

这是我在每次迭代时从网络下载 10 张图像时观察到的时间(以毫秒为单位)。

Elapsed [ImageService]:12.93
Elapsed [ImageService]:13.00
Elapsed [ImageService]:16.74
Elapsed [ImageServiceUnrolled]:16.41
Elapsed [ImageServiceUnrolled]:19.06
Elapsed [ImageServiceUnrolled]:17.88
Elapsed [ImageServiceAsyncStreamUnfolding]:28.76
Elapsed [ImageServiceAsyncStreamUnfolding]
: ImageServiceAsyncStreamUnfolding]:28.47
Elapsed [ImageServiceAsyncStreamContinue]:29.55
Elapsed [ImageServiceAsyncStreamContinue]:28.53
Elapsed [ImageServiceAsyncStreamContinue]:27.05

4

1 回答 1

0

不幸的是,我无法对结果进行基准测试,因为您没有提供示例项目。但是你可以试试这个扩展:

extension Sequence {

    func concurrentMap<T>(
        _ transform: @escaping (Element) async throws -> T
    ) async throws -> [T] {
        let tasks = map { element in
            Task { try await transform(element) }
        }

        return try await tasks.asyncMap { task in
            try await task.value
        }
    }
}

并像这样使用它:

func fetch(from urls: [URL]) async throws -> [UIImage] {
    try await urls.concurrentMap(fetch)
}

我这里唯一的笔记是:

  • 请记住,异步任务可能会导致结果的顺序不同
  • 您可能不需要下载所有图像并考虑使用延迟加载方法
  • 基于网络的任务并不仅仅局限于一件事进行基准测试!如果您考虑在没有其他参数(例如speed,、latency

一些研究表明,一些手册dispatch比 Swift 当前实现的async/await. 但是 Swift 每天都在增强,总有一天,它可能会在底层发生变化。明智地选择你的方法

于 2022-01-08T12:52:43.470 回答