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