启动一些协程,并在超时的情况下将它们全部加入(不取消)

时间:2019-03-10 07:15:57

标签: kotlin kotlin-coroutines

我需要启动许多作业,这些作业将返回结果。

在主代码(不是 协程)中,启动工作后,我需要等待所有工作完成任务 OR 给定的超时时间到期,以先到者为准。

如果我因为所有作业都在超时之前完成而退出等待,那很好,我将收集他们的结果。

但是,如果某些作业花费的时间超过了超时时间,则我的主要功能需要在超时到期后立即唤醒,检查哪些作业已及时完成(如果有)以及哪些作业仍在运行,并且从在那里,没有取消仍在运行的作业。

您如何编码这种等待?

3 个答案:

答案 0 :(得分:0)

您可以尝试使用whileSelectonTimeout子句。但是您仍然必须克服主要代码不是协程的问题。下一行是whileSelect语句的示例。该函数返回一个Deferred,其中包含在超时时间段内评估的结果列表,以及另外Deferred个未完成结果的列表。

fun CoroutineScope.runWithTimeout(timeoutMs: Int): Deferred<Pair<List<Int>, List<Deferred<Int>>>> = async {

    val deferredList = (1..100).mapTo(mutableListOf()) {
        async {
            val random = Random.nextInt(0, 100)
            delay(random.toLong())
            random
        }
    }

    val finished = mutableListOf<Int>()
    val endTime = System.currentTimeMillis() + timeoutMs

    whileSelect {
        var waitTime = endTime - System.currentTimeMillis()
        onTimeout(waitTime) {
            false
        }
        deferredList.toList().forEach { deferred ->
            deferred.onAwait { random ->
                deferredList.remove(deferred)
                finished.add(random)
                true
            }
        }
    }

    finished.toList() to deferredList.toList()
}

在主代码中,您可以使用不鼓励使用的方法runBlocking来访问Deferrred

fun main() = runBlocking<Unit> {
    val deferredResult = runWithTimeout(75)
    val (finished, pending) = deferredResult.await()
    println("Finished: ${finished.size} vs Pending: ${pending.size}")
}

答案 1 :(得分:0)

这是我想出的解决方案。将每个工作与状态配对(以及其他信息):

private enum class State { WAIT, DONE, ... }

private data class MyJob(
    val job: Deferred<...>,
    var state: State = State.WAIT,
    ...
)

并编写一个显式循环:

// wait until either all jobs complete, or a timeout is reached
val waitJob = launch { delay(TIMEOUT_MS) }
while (waitJob.isActive && myJobs.any { it.state == State.WAIT }) {
    select<Unit> {
        waitJob.onJoin {}
        myJobs.filter { it.state == State.WAIT }.forEach { 
            it.job.onJoin {}
        }
    }
    // mark any finished jobs as DONE to exclude them from the next loop
    myJobs.filter { !it.job.isActive }.forEach { 
        it.state = State.DONE
    }
}

初始状态称为WAIT(而不是RUN),因为它不一定意味着作业仍在运行,只是我的循环尚未考虑到它。

我想知道这是否足够习惯,或者是否有更好的方法来编码这种行为。

答案 2 :(得分:0)

解决方案直接来自问题。首先,我们将为任务设计一个挂起函数。让我们看看我们的要求:

  

如果某些作业花费的时间超过了超时时间...而没有取消仍在运行的作业。

这意味着我们启动的作业必须是独立的(而不是子级),因此我们将选择退出结构化并发,并使用GlobalScope启动它们,手动收集所有作业。我们使用async协程生成器是因为我们计划稍后收集R类型的结果:

val jobs: List<Deferred<R>> = List(numberOfJobs) { 
    GlobalScope.async { /* our code that produces R */ }
}
  

启动作业后,我需要等待它们全部完成任务或等待指定的超时时间,以先到者为准。

让我们等待所有这些,并等待超时:

withTimeoutOrNull(timeoutMillis) { jobs.joinAll() }

如果其中一个作业失败,我们将使用joinAll(而不是awaitAll)来避免异常,并使用withTimeoutOrNull来避免超时时出现异常。

  

我的主要功能需要在超时到期后立即唤醒,检查哪些作业已及时完成(如果有)以及哪些作业仍在运行

jobs.map { deferred -> /* ... inspect results */ }
  

在主代码中(不是协程)...

由于我们的主要代码不是协程,因此必须以阻塞的方式等待,因此我们将使用runBlocking编写的代码桥接起来。全部放在一起:

fun awaitResultsWithTimeoutBlocking(
    timeoutMillis: Long,
    numberOfJobs: Int
) = runBlocking {
    val jobs: List<Deferred<R>> = List(numberOfJobs) { 
        GlobalScope.async { /* our code that produces R */ }
    }    
    withTimeoutOrNull(timeoutMillis) { jobs.joinAll() }
    jobs.map { deferred -> /* ... inspect results */ }
}

P.S。我不建议在任何严重的生产环境中部署这种解决方案,因为超时后让后台作业运行(泄漏)将不可避免地对您造成严重的影响。仅在您完全了解这种方法的所有缺陷和风险时,才这样做。