Hello guys, we are pretty new on our team with Kot...
# getting-started
j
Hello guys, we are pretty new on our team with Kotlin and we are trying to understand coroutines, we have donde two different examples and one works and the other doesnt, maybe you guys can shed some light onto why one works and the other doesn't? We have been breaking our heads down here and no luck so far. Here's the example 1, working:
Copy code
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main() = runBlocking {
    val timeInMillis = measureTimeMillis {
        paso1()
        println("----------------------------------------------------------------------")
        val prueba = prueba()
        println("----------------------------------------------------------------------")
        println("size ${prueba.lista.size}")
        println("field 2 ${prueba.mapa["2"]}")
        paso3(prueba.lista)
    }
    println("la operacion duro $timeInMillis ms")
}

suspend fun prueba(): Objeto= coroutineScope {
    val lista = listOf("campo1", "campo2", "campo3", "campo4", "campo5")
    var mapa: HashMap<String, String> = hashMapOf()
    var listaFinal: MutableList<String> = mutableListOf()
    var i = 0
    launch {
        lista.forEach {
            i++
            mapa[i.toString()] = it
            makeObject(it).let { camp ->
                listaFinal.add(camp)
            }

        }
    }

    println("end")
    return@coroutineScope Objeto(mapa = mapa, lista= listaFinal)

}

fun makeObject(i: String): String {
    return "hola $i"
}

fun paso1() {
    println("hice el paso 1")
}


fun paso3(lista: MutableList<String>) {
    lista.forEach {
        println("Resultado $it")
    }
}


data class Objeto(
    val mapa: HashMap<String, String>,

    val lista: MutableList<String>
)
And here's example 2, pretty similar but not working as expected:
Copy code
import kotlinx.coroutines.*

fun main() = runBlocking {
    var result = doWorld()
    println("Hi friends")
    println(result.a)
    println(result.b)
}

suspend fun doWorld(): Result = coroutineScope {

   var res: Result
   var aRes = 0
   var bRes = 0
    val a = listOf(1, 2, 3, 4, 5)


    launch {
        a.forEach {
            println("Hola malditos")

        }
    }
    println("$aRes ---- $bRes")

    return@coroutineScope Result(a = aRes, b = bRes)
}


data class Result(
    var a: Int,
    val b: Int
)
If someone can help us, it would be really great, thanks in advance, thanks!
c
What is expected from the second example? The snippet runs to completion when I run it, so I have no idea what you mean by "it's not working as expected"
j
On the second example we were expecting that the result object of the doWorld method wait for the launch block to complete before returning the result (as the first example does) The main concern is why the first example waits and the second doesn't having the same structure
j
Both examples do the same thing, but none of them work as you expect I believe. The only difference is primitives vs reference types, and this is why you observe different things. In both cases, the result object is constructed concurrently with the `launch`'s execution (in practice it could be constructed even before the launch had a chance to execute anything in its body). The
coroutineScope
block does wait for the launch to complete before returning (in both cases), but the returned value is the one that was constructed earlier. So why do you see apparently different results? The devil is in the details. In the first case, the `Objeto`'s properties are references to objects (a map and a list), so even if the
Objeto
instance is created before anything in the launch executes, it's still the same map and list instances that are returned in the end. In the meantime, the
launch
can fill them in and you can see all the values in the
Objeto
in the end. In the second case, the properties of
Result
are both of primitive types (
Int
). So their value is "copied" from
aRes
and
bRes
when the
Result
intance is constructed. Changing
aRes
and
bRes
later during the `launch`'s execution doesn't have any effect on the properties of the
Result
instance that you created.
c
Got it, thanks for clarifying. I think the main difference here actually isn't that the first example is "waiting" to complete, but rather that the use of mutable variables is messing with your intuition. Notice that in the first example, the output will print
end
immediately, and it's only later that
prueba()
actually returns and gives you the results you're expecting. Technically,
Objeto()
is being created and holding references to mutable collections, and those collections are being updated in the background, and it just so happens that the whole
prueba()
function behaves nicely and things look like they're working like you'd expect. But it's actually not doing what you think, it's just giving you the same results anyway. That's why the second example seems like it doesn't work; it's not the code itself that is wrong, it's your intuition of how coroutines work that is wrong (which is totally understandable, you've got multiple complex systems interacting in subtle and unintuitive ways here!)
The main problem is the
Objeto
holding references to mutable collections, which could be updated at any time by any thread, and it would never know. That is how race conditions are created, and to check this is true, try returning
Objeto(mapa = mapa.toMap(), lista= listaFinal.toList())
instead; that will break the reference to the mutable object, and you'll see that when
Objeto
is created, those collections are still empty, and they will remain empty after
prueba()
returns
What you're expecting to happen is that the coroutines wait until all the
launch { }
blocks have completed before creating
Objeto
. To do that, you'd need to wrap just that one portion of the suspend function in
coroutineScope { }
.
Copy code
suspend fun prueba(): Objeto {
    val lista = listOf("campo1", "campo2", "campo3", "campo4", "campo5")
    var mapa: HashMap<String, String> = hashMapOf()
    var listaFinal: MutableList<String> = mutableListOf()
    var i = 0
    
    coroutineScope { 
        launch {
            lista.forEach {
                i++
                mapa[i.toString()] = it
                makeObject(it).let { camp ->
                    listaFinal.add(camp)
                }

            }
        }
    }

    println("end")
    return Objeto(mapa = mapa.toMap(), lista= listaFinal.toList())
}
j
It should be noted that
coroutineScope { launch { doStuff() } }
is pointless though, because it's equivalent to just
doStuff()
. I think there is also a misconception here about the point of using
launch
. This won't magically parallelize your operations inside the launched block, a single
launch
is still like a single thread. If you want multiple concurrent things to happen, you need multiple
launch
(or if you stick to one
launch
, you need at least something that runs concurrently with the one
launch
to make it useful, which is not the case when there is nothing else in the
coroutineScope
)
💯 2
j
thank you so much for your thorough responses, we will study them at a great extend
j
Glad to help 🙂 Note that if you have more questions specifically about coroutines, there is a #coroutines channel as well
🙏 1