Kotlin协程进度计数器

时间:2018-06-29 16:17:12

标签: kotlin counter progress coroutine kotlin-coroutines

我正在使用async / await发出数千个HTTP请求,并希望有一个进度指示器。我天真地添加了一个,但是注意到所有请求完成后计数器值永远不会达到总数。因此,我创建了一个简单的测试,可以肯定的是,它无法按预期运行:

fun main(args: Array<String>) {
    var i = 0
    val range = (1..100000)
    range.map {
        launch {
            ++i
        }
    }
    println("$i ${range.count()}")
}

输出是这样的,其中第一个数字总是变化的:

98800 100000

我可能缺少有关JVM / Kotlin中的并发/同步的一些重要细节,但不知道从哪里开始。有提示吗?

更新:我最终按照Marko的建议使用频道:

/**
 * Asynchronously fetches stats for all symbols and sends a total number of requests
 * to the `counter` channel each time a request completes. For example:
 *
 *     val counterActor = actor<Int>(UI) {
 *         var counter = 0
 *         for (total in channel) {
 *             progressLabel.text = "${++counter} / $total"
 *         }
 *     }
 */
suspend fun getAssetStatsWithProgress(counter: SendChannel<Int>): Map<String, AssetStats> {
    val symbolMap = getSymbols()?.let { it.map { it.symbol to it }.toMap() } ?: emptyMap()
    val total = symbolMap.size
    return symbolMap.map { async { getAssetStats(it.key) } }
        .mapNotNull { it.await().also { counter.send(total) } }
        .map { it.symbol to it }
        .toMap()
}

3 个答案:

答案 0 :(得分:2)

您正在丢失写操作,因为i++不是原子操作-必须先读取,递增并写回该值-并且您有多个线程同时读写i时间。 (如果不为launch提供上下文,则默认情况下它将使用线程池。)

每当两个线程读取相同的值时,您的计数就会减少1,因为这两个线程都将写入该值加1。

以某种方式进行同步(例如通过使用AtomicInteger)可以解决此问题:

fun main(args: Array<String>) {
    val i = AtomicInteger(0)
    val range = (1..100000)
    range.map {
        launch {
            i.incrementAndGet()
        }
    }
    println("$i ${range.count()}") // 100000 100000
}

也无法保证在打印结果和程序结束时这些后台线程将完成其工作-您可以通过在launch内添加一个很小的延迟(很简单)来轻松地对其进行测试毫秒。这样,将所有内容包装在一个runBlocking调用中是一个好主意,它将使主线程保持活动状态,然后等待协程全部完成:

fun main(args: Array<String>) = runBlocking {
    val i = AtomicInteger(0)
    val range = (1..100000)
    val jobs: List<Job> = range.map {
        launch {
            i.incrementAndGet()
        }
    }
    jobs.forEach { it.join() }
    println("$i ${range.count()}") // 100000 100000
}

答案 1 :(得分:2)

什么才是导致错误方法失败的原因是次要的:首要的是修复方法。

对于这种通信模式,您应该有一个async-await而不是launchactor,所有HTTP作业都将其状态发送到该通信模式。这将自动处理您的所有并发问题。

以下是一些示例代码,这些代码取自您在注释中提供的链接,并适合您的用例。 actor无需某些第三方要求其提供计数器值并使用它更新GUI,而是在UI上下文中运行并更新GUI本身:

import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.channels.*
import kotlin.system.*
import kotlin.coroutines.experimental.*

object IncCounter

fun counterActor() = actor<IncCounter>(UI) {
    var counter = 0
    for (msg in channel) {
        updateView(++counter)
    }
}

fun main(args: Array<String>) = runBlocking {
    val counter = counterActor()
    massiveRun(CommonPool) {
        counter.send(IncCounter)
    }
    counter.close()
    println("View state: $viewState")
}


// Everything below is mock code that supports the example
// code above:

val UI = newSingleThreadContext("UI")

fun updateView(newVal: Int) {
    viewState = newVal
}

var viewState = 0

suspend fun massiveRun(context: CoroutineContext, action: suspend () -> Unit) {
    val numCoroutines = 1000
    val repeatActionCount = 1000
    val time = measureTimeMillis {
        val jobs = List(numCoroutines) {
            launch(context) {
                repeat(repeatActionCount) { action() }
            }
        }
        jobs.forEach { it.join() }
    }
    println("Completed ${numCoroutines * repeatActionCount} actions in $time ms")
}

运行它打印

Completed 1000000 actions in 2189 ms
View state: 1000000

答案 2 :(得分:1)

您读过Coroutines basics吗?与您的问题完全相同:

val c = AtomicInteger()

for (i in 1..1_000_000)
    launch {
        c.addAndGet(i)
    }

println(c.get())
     

这个示例对我来说不到一秒钟就完成了,但是它打印了一些任意数字,因为一些协程在main()打印结果之前没有完成。

因为launch没有被阻止,所以不能保证所有协程都将在println之前完成。您需要使用async,存储Deferred对象和await来完成它们。