【问题标题】:Async await list detail异步等待列表详细信息
【发布时间】:2021-11-02 00:03:52
【问题描述】:

我正在尝试通过多步异步调用获取详细的项目列表。首先,我获取一个标识符列表,然后获取每个标识符的详细信息。

func fetchList() async -> [UUID] {
    // some async network call here
    await Task.sleep(2)
    return [UUID(), UUID(), UUID()]
}

func fetchDetail(forUUID uuid: UUID) async -> String {
    // some async network call here
    await Task.sleep(1)
    return "\(uuid.uuidString.reversed())"
}

这些功能单独工作正常,但当我想尝试使用列表中的.map 时,我被卡住了。

Task {
    let list = await fetchList()
    let details = await list.map { fetchDetail(forUUID: $0) } // Does not work
}

我确实发现了 withTaskGroup 函数,当然我可以异步追加到一组项目,但我想知道是否有更好的方法来实现此功能。

【问题讨论】:

  • 如果第一个方法真的返回一个简单的数组,那么它是异步的这一事实几乎是无关紧要的;这些是您唯一的选择:您可以一次或同时处理一个列表项。但是既然你说你知道任务组,并且由于任务组正是如何同时处理列表的所有成员,所以很难看出问题是什么。

标签: ios swift async-await concurrency


【解决方案1】:

异步代码的映射需要AsyncSequence 才能工作。

struct DelayedUUIDs: AsyncSequence {
  typealias Element = UUID

  struct DelayedIterator: AsyncIteratorProtocol {
    private var internalIterator = [UUID(), UUID(), UUID()].makeIterator()

    mutating func next() async -> UUID? {
      await Task.sleep(1_000_000_000)
      return internalIterator.next()
    }
  }

  func makeAsyncIterator() -> DelayedIterator {
    DelayedIterator()
  }
}

func fetchList() async -> DelayedUUIDs {
    // some async network call here
    await Task.sleep(1)
    return DelayedUUIDs()
}

func fetchDetail(forUUID uuid: UUID) async -> String {
    // some async network call here
    await Task.sleep(1)
    return "ID: \(uuid.uuidString)"
}


Task {
    let list = await fetchList()
    let details = list.map({ id in await fetchDetail(forUUID: id) })
    for await value in list {
      print(Date())
      print(value)
    }
    for await value in details {
      print(Date())
      print(value as String)
    }
}

UPD:通用AsyncSequence 示例。

struct DelayedArr<E>: AsyncSequence, AsyncIteratorProtocol {
    typealias Element = E
    var current = 0
    var elements: [E]
    mutating func next() async -> E? {
        defer { current += 1 }
        await Task.sleep(1_000_000_000)
        guard current < elements.count else { return nil }
        return elements[current]
    }

    func makeAsyncIterator() -> DelayedArr {
        self
    }
}

let sequence = DelayedArr(elements: [1, 2, 3 ,4, 5])
func asyncIntToString(i: Int) async -> String {
    await Task.sleep(2_000_000_000)
    return String(i)
}

let asyncMapSequence1 = sequence.map{ await asyncIntToString(i: $0)} //asyncronous code
let asyncMapSequence2 = sequence.map{ String($0) } // or syncronous code

Task {
    for await value in asyncMapSequence1 {
        print(Date())
        print(value)
    }
    for await value in asyncMapSequence2 {
        print(Date())
        print(value)
    }
}

【讨论】:

  • 有没有办法让这个通用?
  • 不确定我是否正确理解了这个问题。添加了通用AsyncSequence 示例@Bram。
【解决方案2】:

在您实际给出的示例中,fetchList 一次性返回一个值数组。因此,它是异步的这一事实实际上是无关紧要的。关键是我们现在从一个数组开始,我们希望通过调用异步方法来处理它的元素。

因此,您基本上有两种选择:您可以一个一个地处理元素,等待每个元素,或者您可以同时处理它们。如果您希望同时处理它们,那就是任务组的用途,并且您声称已经知道这一点,因此无需多说。这正是 WWDC 视频中关于该主题的示例,以及我在 https://www.biteinteractive.com/swift-5-5-asynchronous-looping-with-async-await/ 的教程。

请注意,正如视频和我的教程中所指出的那样,如果您选择同时处理这些值,则结果可以按任何顺序返回,因此如果您不想这样做,则必须采取一些额外的步骤失去每个原始 URL 与其处理结果之间的连接。通常的解决方案是构建一个字典或只是一个对的元组。在我的书中,我使用字典方法。改编自该代码,您可以执行以下操作:

func processManyURLs(urls: [URL]) async -> [URL:String] {
    return try await withTaskGroup(
        of: [URL:String].self, 
        returning: [URL:String].self) { group in
            var result = [URL:String]()
            for url in urls {
                group.addTask {
                    return [url: await fetchDetail(url: url)]
                }
            }
            for try await d in group {
                result.merge(d) {cur,_ in cur}
            }
            return result
    }
}

【讨论】:

    猜你喜欢
    • 2016-10-01
    • 2021-08-25
    • 2011-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多