https://kotlinlang.org logo
#coroutines
Title
# coroutines
s

Stephan Schroeder

05/19/2020, 8:25 AM
Is
runBlocking
an expensive operation? The docs seem to suggest that it’s a good idea to use it once in the main-method and than stay in suspend-country for the rest of the codebase. But how about sprinkling in a bit of concurrent computation here and there with snippets like this:
Copy code
val (a, b) = runBlocking(Dispatchers.Default) {
        val aDeferred = async {getA()}
        val aDeferred = async {getB()}
        return@runBlocking aDeferred.await() to bDeferred.await()
}
Doable or bad idea?
o

octylFractal

05/19/2020, 8:50 AM
the overhead is pretty small, but it does block the thread
it would probably be more efficient to put as much of the function inside the runBlocking as you can
s

Stephan Schroeder

05/19/2020, 9:41 AM
sounds like “doable” then 🙂
g

gildor

05/19/2020, 9:54 AM
I would also removed default dispatcher, it’s not really necessary there
k

Kroppeb

05/19/2020, 10:32 AM
It makes getA and getB run parallel though?
g

gildor

05/19/2020, 11:36 AM
depends on how a and b are implemented
apparently, if those a and b are proper suspend functions they will run in parallel
if they are blocking functions, one can run them in IO thread to make them parallel
async(IO) { getA() }
u

Ugi

05/19/2020, 5:40 PM
apparently, if those a and b are proper suspend functions they will run in parallel
I don't think
getA()
or
getB()
being suspend function affects them executing in parallel in this case. As far as I understand it, it will depend on the dispatcher.
g

gildor

05/20/2020, 2:17 AM
if function is proper suspend function, so it doesn’t block the thread, they will run in parallel, with any dispatcher (except some non-existing, which limits amount of coroutines in paralell)
o

octylFractal

05/20/2020, 2:19 AM
actually, if you were to drop the
Dispatchers.Default
, they would run concurrently but not in parallel, because there is only one thread for
runBlocking
g

gildor

05/20/2020, 2:20 AM
not necessary, it depends on getA() and getB implementation
just imagine you replace getA and getB with delay, they will run perfectly in parallel
also with any other, even blocking function, if it wrapped to own dispatcher it will run in parallel, which must be the case for all properly designed suspend functions
so Dispatchers.Default will not affect behaviour, until functions are blocking
o

octylFractal

05/20/2020, 2:24 AM
I think you have a misunderstanding of the term 'parallel' -- it means that code is executing at the same time, which
delay
does not represent, no code is executed while the delay is happening (i.e., no CPU usage). using
delay
represents a concurrent process. take for example Javascript, nothing is ever done in parallel with javascript, it is all one single-threaded event loop. that is what I mean by
runBlocking
(with no specified dispatcher) being concurrent but not parallel
g

gildor

05/20/2020, 2:24 AM
I know what is parallel
okay, maybe delay is not perfect example, just because nothing is actually happening in parallel, they just run concurrently. Now see:
Copy code
suspend fun getA() = IO { File("A").readBytes() }
suspend fun getB() = IO { File("B").readBytes() }
o

octylFractal

05/20/2020, 2:27 AM
right, but that introduces a new dispatcher, I am specifically talking if you didn't add the I/O dispatcher
g

gildor

05/20/2020, 2:27 AM
With any dispatcher in runBlocking they will run in parallel
This is why I said that it depends on getA and getB implementation, isn’t it?
o

octylFractal

05/20/2020, 2:28 AM
that is true, but I want to be clear that it requires them to use a different dispatcher
g

gildor

05/20/2020, 2:28 AM
or if they are asyncronous, right?
You trying to say that parallel execution impossible in the same dispatcher with the same thread, but it’s not exactly true
for example you have such things as animations or anything else backed by event queue
of course you may argue it still not parallel, but it does works in parallel, even tho under the hood it dispatched using an even queue
so on practice, from point of view of consumer of this API it run in parallel, exactly what was original intent of this pattern
it’s also true for delay, even tho, I agree, it’s probably to simplistic example, yeah, “Code is not executing”, as you said, but task is executing
o

octylFractal

05/20/2020, 2:32 AM
this code demonstrates the issue:
Copy code
fun main() {
    runBlocking {
        val a = async {
            println("Computing [A]...")
            // be blocking
            Thread.sleep(1000)
            println("Finish [A]!")
            100
        }
        val b = async {
            println("Computing [B]...")
            // be blocking
            Thread.sleep(1000)
            println("Finish [B]!")
            200
        }
        println("Technically concurrent! I can execute with those queued up.")
        println(a.await() + b.await())
    }
}
both computations are not done in parallel, though they are both done concurrently, i.e. it does not stop you from setting up the code to execute later obviously, just using
Dispatchers.Default
here is enough to alleviate the problem
g

gildor

05/20/2020, 2:33 AM
I mentioned this problem in my first message, it will run in parallel only if functions are not blocking
o

octylFractal

05/20/2020, 2:33 AM
right, but all code at some level is blocking, it's just for how long
g

gildor

05/20/2020, 2:33 AM
Dispatchers.Default will solve it, I agree, but I would say that it’s bad pattern
especially if getA and getB are suspend
right, but all code at some level is blocking, it’s just for how long
Are we talking about absolutes or about practical usage?
it reminds my arguing that non-blocking IO is actually blocking on level of sockets in operation system
Returning to our discussion, I will agree with you, passing dispatcher to runBlocking may be useful if you call a lot of blocking function in parallel inside
But on abstract example, I would say that it’s not the cleanest solution, for suspend functions it shouldn’t be necessary, for blocking functions I would rather wrap them explicitly to new
runInterruptible(IO)
, so usually it’s better to extract them to own suspend function. So it makes usage of dispatcher for runBlocking unnecessary
o

octylFractal

05/20/2020, 2:54 AM
I am kinda talking about practical usage here -- even some operations on non-IO become sufficiently blocking that their performance is measurable. I worked out this example of sorting large lists: https://gist.github.com/octylFractal/5443dc85fbf3673553f30f68fa888f21
results on my 12 core machine are noticeable:
Copy code
872ms for event loop
346ms for Default dispatcher
basically, I just want people to be aware of the fact that the event loop does not run anything in parallel -- if you delegate to another dispatcher, then it can await the results from said dispatcher concurrently but the event loop itself is not parallel-capable
g

gildor

05/20/2020, 2:56 AM
But this operation is blocking
you do big chunk of computation
o

octylFractal

05/20/2020, 2:56 AM
yes, that's what I'm getting at
g

gildor

05/20/2020, 2:56 AM
so you should wrap this chunk to Dispatcher.Default
o

octylFractal

05/20/2020, 2:57 AM
right, but not everyone does that
g

gildor

05/20/2020, 2:57 AM
this chunk, not expect that some user of this API will do this for you
Okay, okay, I agree with you
o

octylFractal

05/20/2020, 2:57 AM
I'm just saying be aware of how things are implemented
g

gildor

05/20/2020, 2:57 AM
maybe introducing bad pattern to fix another bad code is fine
I would rather tried to avoid both
o

octylFractal

05/20/2020, 2:57 AM
yes, certainly
👍 1
k

Kroppeb

05/20/2020, 7:22 AM
so you should wrap this chunk to Dispatcher.Default
I did not know that was best practice, switching dispatcher close to the expensive code itself.
o

octylFractal

05/20/2020, 7:23 AM
I would think it is, if you're already on the dispatcher it's nearly free
g

gildor

05/20/2020, 7:23 AM
if you do not wrap some heavy code to own dispatcher, it may cause issues for consumer
when invoke suspend function you expect that it will not block for significant amount of time
u

Ugi

05/20/2020, 7:37 AM
basically, I just want people to be aware of the fact that the event loop does not run anything in parallel -- if you delegate to another dispatcher, then it can await the results from said dispatcher concurrently but the event loop itself is not parallel-capable
This is the gist of it. I think it's also important to be aware of what your default dispatcher is especially in multi platform environment, because on JVM it's backed by a thread pool, so you will get parallel execution, but on JS (always single-thread) and Native (uses SingleThreadDispatcher at least in 1.3.5-native-mt-1.4M1) you will get concurrent but not parallel execution.
👍 1
r

rbares

05/20/2020, 3:23 PM
Likewise if using
kotlinx-coroutines-android
you will find that
Dispatchers.Main
is backed by a
Handler
using
Looper.getMainLooper()
, which is another example of a single-threaded event loop. This same logic also applies to dispatchers based on a single-threaded Executor. The lack of parallel execution in these cases is one of the reasons you can use thread confinement to safely mutate shared state without need for other synchronization mechanisms.
5 Views