我正在使用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()
}
答案 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
而不是launch
或actor
,所有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
来完成它们。