32

Can you please explain me what is the difference between these two blocks of code. First time prints 421, but second prints 606. Why first one is parallel and second one is sequential?

fun main(args: Array<String>) = runBlocking {

    var time = measureTimeMillis {
        val one = async { one() }
        val two = async { two() }
        val int1 = one.await()
        val int2 = two.await()
        println(int1 + int2)

    }

    println(time)


    time = measureTimeMillis {
        val one = async { one() }.await()
        val two = async { two() }.await()
        println(one + two)

    }

    print(time)
}

suspend fun one(): Int {
    delay(200)
    return 12
}

suspend fun two(): Int {
    delay(400)
    return 23
}
Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
toffor
  • 1,219
  • 1
  • 12
  • 21
  • I'm not sure how you could possibly get either of those values. 12 + 23 = 35. How in the world you are getting 421 & 606 is beyond me. Maybe confirm what you typed in your question and then I'll see if I can help. Await on an async is typically replaced with "withContext(yourProviderThread){} and then no await is required, but either way async { someMethod }.await() is acceptable, if you don't await in line, then you can easily do one.await() + two.await() and that should give the same value as well. But hard to speculate when your output doesn't line up. – Sam Sep 17 '18 at 14:09
  • 5
    the 421/606 were the milliseconds it took to run variant one and variant two ;-) – Roland Sep 17 '18 at 14:10

3 Answers3

57
val one = async { one() }
val two = async { two() }
val int1 = one.await()
val int2 = two.await()

What this does:

  1. spawn task one
  2. spawn task two
  3. await on task one
  4. await on task two

val one = async { one() }.await()
val two = async { two() }.await()

What this does:

  1. spawn task one
  2. await on task one
  3. spawn task two
  4. await on task two

There's no concurrency here, it's purely sequential code. In fact, for sequential execution you shouldn't even use async. The proper idiom is

val one = withContext(Dispatchers.Default) { one() }
val two = withContext(Dispatchers.Default) { two() }
Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • 3
    What's the difference between that `withContext` snippet and simply `val one = one(); val two = two()`? – Oliver Charlesworth Sep 17 '18 at 18:27
  • 5
    We assume `one()` and `two()` are blocking functions and the calling thread has an event loop (most typcially, it's the UI thread). Writing just `one()` blocks the current thread and writing `withContext(Default) { one() }` transfers the blocking call to a threadpool while the current thread can go on processing other events until the result of `withContext()` is ready. – Marko Topolnik Sep 18 '18 at 07:25
  • 1
    Note that, even though the code is executing sequentially, it's not executing on the same thread all the time. A coroutine jumping between threads like this is in close analogy to a thread jumping between CPU cores. Threads are to coroutines what CPU cores are to threads (modulo preemptive scheduling). – Marko Topolnik Sep 18 '18 at 07:35
8

In the first variant you get a Deferred<Int> for both async-calls. As the documentation of Deferred shows so nicely there are several states the deferred object might be in. From the outside that state is now either new or active but surely not completed yet. On your second variant however the first async-await needs a completed state already otherwise you could not have any value there. However on your async{one()}.await() the second async is not known yet. Note also that the return value of await() is now Int and not Deferred anymore, so the coroutine must have been executed by then. Check also the documentation of await().

In other words:

val one = async { one() }
val two = async { two() }

Both one and two are now Deferred<Int>. None has been called yet (or might have been called yet). As soon as you call one.await() it may start already both one and two, just because it has the resources for it (even if you didn't use two.await() anywhere in your code).

On the second variant however:

val one = async { one() }.await()
val two = async { two() }.await()

Even though it creates a coroutine for async {one()} it must set a value to one immediately, because you are calling await() on it. The types of one and two are both Int. So as soon as the first of those lines is hit, the async code needs to be executed immediately. By then nobody knows that another asynchronous call has to be executed as we wait for the value of the first. If the first wouldn't have an await, the coroutine would again be executed in parallel, e.g.:

val one = async { one() }
val two = async { two() }.await()

will execute one() and two() in parallel.

So maybe this can be summarized to: only those coroutines can be executed in parallel on an await, that are known/spawned by then.

Roland
  • 22,259
  • 4
  • 57
  • 84
  • It's not so much about it *knowing*; it's more about you *telling*. In the second variant, you're telling the compiler to await the execution of the first coroutine before moving on (before creating the second coroutine). `await` is a non-suspending function. – Oliver Charlesworth Sep 17 '18 at 18:23
  • `await` is actually a suspending function. – Marko Topolnik Sep 18 '18 at 07:27
  • ok... "knowing" maybe was the wrong word... one coroutine does not know anything about the other... just meant to say something similar... e.g. all coroutines that were spawned (I like that formulation much better) are known by the "coroutine executor" and can therefore be run as soon as the first await is hit... this will not work for coroutines that are not known by then... e.g. that are initialized after the await... – Roland Sep 18 '18 at 07:38
6

The thumb-rules:

  • Use withContext when you do not need the parallel execution.
  • Use async only when you need the parallel execution. Both withContext and async can be used to get the result which is not possible with the launch.
  • Use withContext to return the result of a single task.'
  • Use async for results from multiple tasks that run in parallel.

Check this for more details

MohamedHarmoush
  • 1,033
  • 11
  • 17