Channel possibly not acting fair
See original GitHub issueI noticed a few scenarios with channels where they do not seem to be fair and are filling their buffer in the wrong order. The main gist of the post can be seen from the first example, while the additional examples provide alternate scenarios with more unexpected outputs.
suspend fun sendString(channel: SendChannel<String>, s: String) {
while (true) {
channel.send(s)
}
}
fun main(args: Array<String>) = runBlocking<Unit> {
val channel = Channel<String>()
launch(coroutineContext) { sendString(channel, "foo") }
launch(coroutineContext) { sendString(channel, "BAR!") }
repeat(6) { // receive first six
println(channel.receive())
}
}
Outputs:
foo
foo
BAR!
foo
foo
BAR!
If the channel is given a buffer, the buffer is then filled with as many foo’s as it can hold, and then a single BAR! which causes an output of the following when the channel has a buffer of 4:
foo
foo
foo
foo
foo
foo
The buffer is always filled to its max capacity with “foo” and only gets a “BAR!” afterwards at which point the send(“BAR!”) is suspended until the multiple foo’s in the buffer are drained.
Filling the buffer like this also has some other weird results when a delay is added either before or after channel.receive() is called, as seen below:
fun main(args: Array<String>) = runBlocking<Unit> {
val channel = Channel<String>(2)
launch(coroutineContext) { sendString(channel, "foo") }
launch(coroutineContext) { sendString(channel, "BAR!") }
repeat(6) { // receive first six
delay(1L)
println(channel.receive())
}
}
Outputs:
foo
BAR!
foo
BAR!
foo
BAR!
Above is what I anticipated the original code to output since that seems more like the channel is acting fair.
fun main(args: Array<String>) = runBlocking<Unit> {
val channel = Channel<String>()
launch(coroutineContext) { sendString(channel, "foo") }
launch(coroutineContext) { sendString(channel, "BAR!") }
repeat(6) { // receive first six
println(channel.receive())
delay(1L)
}
}
Outputs:
foo
foo
BAR!
foo
BAR!
foo
From the code above and through some debugging, send(“foo”) is getting called, then it is getting to send(“foo”) a second time at which point it suspends, then it goes to send(“BAR!”) and suspends which causes the next strings in the buffer to be ordered such that “foo” is before “BAR!” for the first two items emitted.
And the last scenario:
fun main(args: Array<String>) = runBlocking<Unit> {
val channel = Channel<String>()
launch(coroutineContext) { sendString(channel, "foo") }
launch(coroutineContext) { sendString(channel, "BAR!") }
delay(1L)
repeat(6) { // receive first six
println(channel.receive())
}
}
Outputs:
foo
BAR!
foo
foo
BAR!
foo
This case was the weirdest as the execution of the while loop and the suspensions from send in sendString were in a very weird order that caused two “foo”'s to output in position 2 and 3.
Sorry for the long post, I noticed this while playing around with coroutines and did not think it was intended so I thought I would post here. If this is intended or there is a silly I’m making, any explanation is appreciated. Thanks.
Issue Analytics
- State:
- Created 6 years ago
- Reactions:15
- Comments:13 (9 by maintainers)
Top GitHub Comments
This is a side effect of the fact that all the coroutines run under the same context in the same thread. Let me provide a step by step explanation of the first scenario (with unbuffered rendezvous channel):
launch
(let’s call them “foo” coroutine and “bar” coroutine).The resulting “foo”, “foo”, "bar, “foo”, “foo”, “bar”, etc does look counter-intivive, though. I’ll keep this issue open for two reasons:
No update so far. We’ve been thinking about fairness of the channels, though, and here are some thoughts:
So, the current thinking that that we should make channels “partially fair” – yield periodically (say on every 32 sends of something like that). It is not easy to add to the current design of channel, but as we work on a different channel implementation that feature can be quite easily incorporated at little additional cost.