Kotlin协程进程计数器

Kotlin协程进程计数器,kotlin,counter,progress,coroutine,kotlin-coroutines,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 }

我正在使用async/await发出数千个HTTP请求,希望有一个进度指示器。我以一种天真的方式添加了一个,但注意到当所有请求都完成时,计数器值永远不会达到总数。因此,我创建了一个简单的测试,果然,它没有按预期工作:

fun main(args: Array<String>) {
    var i = 0
    val range = (1..100000)
    range.map {
        launch {
            ++i
        }
    }
    println("$i ${range.count()}")
}
我可能遗漏了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()
}
/**
*异步获取所有符号的统计信息,并发送请求总数
*每次请求完成时都会发送到“计数器”通道。例如:
*
*val抵消器=参与者(UI){
*变量计数器=0
*用于(通道中的总计){
*progressLabel.text=“${++counter}/$total”
*         }
*     }
*/
暂停趣味getAssetStatsWithProgress(计数器:SendChannel):映射{
val symbolMap=getSymbols()?.let{it.map{it.symbol to it}.toMap()}?:emptyMap()
val total=symbolMap.size
返回symbolMap.map{async{getAssetStats(it.key)}
.mapNotNull{it.await()。还有{counter.send(total)}
.map{it.symbol to it}
.toMap()
}

您正在丢失写操作,因为
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
}
你读过吗?与您的问题完全相同:

对于我来说,这个示例在不到一秒钟的时间内完成,但它会打印一些任意数字,因为有些协程在main()打印结果之前不会完成


因为
launch
没有阻塞,所以不能保证所有的协同程序都会在
println
之前完成。您需要使用
async
,存储
延迟的
对象并等待它们完成。

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

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

下面是一些示例代码,取自您在注释中提供的链接,并根据您的用例进行了调整。与某些第三方询问计数器值并用其更新GUI不同,actor在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")
}

我注意到,
launch(Unconfined)
在这个简单的示例中为我提供了窍门,但从《协同路由指南》中可以看出:“当协同路由不消耗CPU时间,也不更新任何限制在特定线程内的共享数据(如UI)时,Unconfined dispatcher是合适的。”我肯定两者都在做:)如果你能提供一个代码片段,那就太好了。我刚刚试着使用一个带计数器的演员(类似于那里的方式),但遇到了同样的问题。这太完美了!感谢您发布代码片段。因为我也想显示总数,所以我将
IncCounter
替换为简单的
Int
,这是我们将要执行的异步请求数。因此,每次请求完成时,我只需向通道发送相同的号码。不需要特殊类型,我们可以执行
updateView(“${++counter}/$msg”)
。再次感谢!请注意,
count()
对于范围来说有些低效,因为它会迭代所有值。
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
}
val c = AtomicInteger()

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

println(c.get())
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