带有翻新,协程和挂起功能的并行请求

时间:2019-11-01 11:38:33

标签: android kotlin coroutine kotlin-coroutines suspend

我正在使用Retrofit,以便发出一些网络请求。我还将协同程序与“暂停”功能结合使用。

我的问题是:有没有一种方法可以改善以下代码。这个想法是并行启动多个请求,并等待它们全部完成,然后再继续执行该功能。

lifecycleScope.launch {
    try {
        itemIds.forEach { itemId ->
            withContext(Dispatchers.IO) { itemById[itemId] = MyService.getItem(itemId) }
        }
    } catch (exception: Exception) {
        exception.printStackTrace()
    }

    Log.i(TAG, "All requests have been executed")
}

(请注意,“ MyService.getItem()”是一个“暂停”功能。)

我猜在这种情况下,有比 foreach 更好的东西。

有人有主意吗?

2 个答案:

答案 0 :(得分:5)

我已经准备了三种解决方法,从最简单到最正确的一种。为了简化方法的表示,我提取了以下通用代码:

lifecycleScope.launch {
    val itemById = try {
        fetchItems(itemIds)
    } catch (exception: Exception) {
        exception.printStackTrace()
    }
    Log.i(TAG, "Fetched these items: $itemById")
}

在继续之前,请注意以下一般事项:您的getItem()函数是可挂起的,您无需将其提交给IO调度程序。您所有的协程都可以在主线程上运行。

现在让我们看看如何实现fetchItems(itemIds)

1。简单forEach

这里我们利用了所有协程代码都可以在主线程上运行的事实:

suspend fun fetchItems(itemIds: Iterable<Long>): Map<Long, Item> {
    val itemById = mutableMapOf<Long, Item>()
    coroutineScope {
        itemIds.forEach { itemId ->
            launch { itemById[itemId] = MyService.getItem(itemId) }
        }
    }
    return itemById
}

coroutineScope将等待您launch里面的所有协程。即使它们都同时运行,启动的协程仍会分派到单个(主)线程,因此从每个线程更新映射不会出现并发问题。

2。线程安全变体

它利用了单线程上下文的属性这一事实可以看作是第一种方法的局限性:它不能推广到基于线程池的上下文。我们可以依靠async-await机制来避免这种限制:

suspend fun fetchItems(itemIds: Iterable<Long>): Map<Long, Item> = coroutineScope {
    itemIds.map { itemId -> async { itemId to MyService.getItem(itemId) } }
            .map { it.await() }
            .toMap()
}

这里,我们依靠Collection.map()的两个非显而易见的属性:

  1. 它急切地执行所有转换,因此对Deferred<Pair<Long, Item>>集合的第一次转换已完全完成,然后进入第二阶段,我们等待所有这些转换。
  2. 这是一个内联函数,即使该函数本身不是suspend fun并获得不可悬浮的lambda (Deferred<T>) -> T,它也允许我们在其中编写可悬挂的代码。

这意味着所有提取操作是同时完成的,但是地图被组装在一个协程中。

3。改进的并发控制的基于流的方法

以上内容为我们解决了并发问题,但它没有任何背压。如果您的输入列表非常大,则需要限制同时发出的网络请求数量。

您可以使用基于Flow的习惯用法来做到这一点:

suspend fun fetchItems(itemIds: Iterable<Long>): Map<Long, Item> = itemIds
        .asFlow()
        .flatMapMerge(concurrency = MAX_CONCURRENT_REQUESTS) { itemId ->
            flow { emit(itemId to MyService.getItem(itemId)) }
        }
        .toMap()

神奇之处在于.flatMapMerge操作中。给它提供一个函数(T) -> Flow<R>,它将在所有输入上顺序执行它,但是它将并发地收集它得到的所有流。请注意,我无法将flow { emit(getItem()) } }简化为flowOf(getItem()),因为getItem()在收集流时必须被懒惰地调用。

Flow.toMap()当前未在标准库中提供,因此它是:

suspend fun <K, V> Flow<Pair<K, V>>.toMap(): Map<K, V> {
    val result = mutableMapOf<K, V>()
    collect { (k, v) -> result[k] = v }
    return result
}

答案 1 :(得分:1)

如果您正在寻找一种更好的编写方式并消除foreach

lifecycleScope.launch {
    try {

        itemIds.asFlow()
               .flowOn(Dispatchers.IO) 
               .collect{ itemId -> itemById[itemId] = MyService.getItem(itemId)}

    } catch (exception: Exception) {
        exception.printStackTrace()
    }

    Log.i(TAG, "All requests have been executed")
}

也请查看lifecycleScope,我怀疑它正在使用Dispatchers.Main。在这种情况下,您可以删除此.flowOn(Dispatchers.IO)额外的调度程序声明。

有关更多信息:Kotlin Asynchronous Flow