I have a class with 2 lazy properties and I need t...
# coroutines
d
I have a class with 2 lazy properties and I need to compute them in parallel. My idea was to use coroutines for this. Here is a pseudo-code - it doesn't compile but hopefully conveys the idea:
Copy code
class MyClass(val scope: CoroutineScope) {
  val p1 = lazy { scope.async { someExpensiveComputation() } }
  val p2 = lazy { scope.async { anotherExpensiveComputation() } }

  val p3 = lazy { useBothProperties(p1.await(), p2.await()) }
  val p4 = lazy { useSingleProperty(p1.await()) }
}

fun main() {
  runBlocking {
    val myClass = MyClass(this)
    myClass.p3 + myClass.p4
  }
}
How can I achieve this with coroutines? Or is there perhaps another way how to achieve my main goal (first sentence)?
s
This looks like a good approach, you probably just need to add
scope.async
to the provider function for p3 and p4 as well (and then call await() when accessing them)
d
I can do that but it seems unnecessary because I don't need p3 and p4 to be computed asynchronously. Isn't there a way that better shows the intention?
s
Since
await
is a suspend function, you'll need to be in a coroutine context to call them. Also, when you use these in main, do you expect the method to suspend while the values are computed in the expensive operations? If so, you'll need to be in a coroutine context there, too, right? It looks to me like you'll need (want) p3 and p4 to be `Deferred`s, just like p1 and p2.
(Which you would get by calling
scope.launch
.)
d
I have put more details in the
main
function so that it's hopefully clearer what I actually want to achieve.
In essence, I want to perform some synchronous operation with p3 and p4, ensuring that p1 and p2 are computed in parallel.
s
In this case, I think the correct approach would be
Copy code
class MyClass(val scope: CoroutineScope) {
  val p1 = lazy { scope.async { someExpensiveComputation() } }
  val p2 = lazy { scope.async { anotherExpensiveComputation() } }

  val p3 = lazy { scope.async { useBothProperties(p1.await(), p2.await()) } }
  val p4 = lazy { scope.async { useSingleProperty(p1.await()) } }
}

fun main() {
  runBlocking {
    val myClass = MyClass(this)
    myClass.p3.await() + myClass.p4.await()
  }
}
Oh, sorry, no, you also want
p3
and
p4
to run in parallel?
s
If you want to run two tasks in parallel, and get their results , you always need at least one task to wait for another, so there's always going to be some suspending (or blocking) involved on the caller's side
d
Oh, sorry, no, you also want
p3
and
p4
to run in parallel?
No, that's not required.
Ok, I think you answered my question. I'm just surprised that I need to artificially use async->await in p3, p4 even if I actually don't need that functionality.
s
You need it to be able to
await
, essentially. You can also get away with passing `Deferred`s down to
useBothProperties
and
useSingleProperty
, but that's probably not what you want, either.
d
You need it to be able to
await
I understand that and I also understand that I need a coroutine scope for that. It just seems a little strange that once I have that scope, I can only await p1 and p2 by wrapping this with another
async->await
🙂
s
I'm sorry, but I don't quite understand what you mean with “once you have that scope”.
Also, I take back what I said earlier about passing in `Deferred`s. I don't see how that would work, either, seeing as there's no
map
or anything on
Deferred
. 😅
d
Ok, so I can do this, right?
Copy code
class MyClass(val scope: CoroutineScope) {
  val p1 = lazy { scope.async { someExpensiveComputation() } }
  val p2 = lazy { scope.async { anotherExpensiveComputation() } }

  val p3 = lazy { 
    scope.async {
      useBothProperties(p1.await(), p2.await())
    }.await()
  }
  val p4 = lazy { 
    scope.async {
      useSingleProperty(p1.await())
    }.await()
  }
}
I'm just thinking that those `scope.async`s in p3 and p4 are strange. That's all.
s
You won't really be able to do that, either, no. Your problem remains the same; you're calling
await
outside of a coroutine context (outside of
scope.async {}
).
d
Oh, I see
But that's basically my point. I don't technically need async/await for p3/p4 I just need to provide a scope so that p1 and p2 can be computed (in parallel). That other async/await for p3/p4 just complicated things 🙂
s
Hmm … is it necessary for
p3
and
p4
to be properties, or can they be functions
p3()
and
p4()
?
Cause while property getters can't suspend, functions can:
Copy code
suspend fun p3() = useBothProperties(p1.await(), p2.await())
suspend fun p4() = useSingleProperty(p1.await())
Of course, this removes the laziness of
p3
and
p4
, so you'd need to handle that manually.
d
Will think about it and get back to you. Thank you!
s
Sure thing! I hope I was able to clarify a little bit, at least.
d
sounds more like you want to use a cold flow than a lazy param.
c
You don't need the
by lazy {}
, you can:
Copy code
val p1 = scope.async(start = CoroutineStart.LAZY) { someExpensiveComputation() }
s
True, but: https://github.com/Kotlin/kotlinx.coroutines/issues/4202 (tl;dr: CoroutineStart.LAZY and lazy {} do basically the same thing, and lazy{} causes less problems, so it's probably the better choice)
👀 2
c
> But that's basically my point. I don't technically need async/await for p3/p4 I just need to provide a scope so that p1 and p2 can be computed (in parallel). That other async/await for p3/p4 just complicated things 🙂 What do you expect to happen when
p3
/`p4` are accessed when
p1
/`p2` have not finished computing yet?
d
So my actual situation is that somewhere in the code flow, I have the
scope
val (from the top-level
runBlocking
) and want to evaluate
myClass.p3
with this scope. Obviously, I cannot use async/await because for
await
I would need another scope...
I can make the p3/p4 vals async as well, but I don't see how that would help.
It seems that I need something like
Copy code
withScope(scope) {
   myClass.p3.await()
}
(assuming
p3
is
Deferred
)
c
Can you clarify what you expect to happen when
p3
/`p4` are accessed when
p1
/`p2` have not finished computing yet?
d
p3
/`p4` should wait for
p1
/`p2` to finish. AFAIK that's written in my original example, e.g.
Copy code
val p3 = lazy { useBothProperties(p1.await(), p2.await()) }
c
If you want accesses to
p3
/`p4` to
suspend
, you don't a choice except having them be
Deferred
or
suspend fun
, yeah
d
Still, how do I do that? I.e. p3 is
Deferred
but how do I
await
on it (e.g. in
main
) if I only have the
scope
?
c
I don't understand the question,
yourClass.p3.await()
?
d
Let me clarify my actual usage (simplified):
Copy code
fun main() {
  val myClass = runBlocking {
     MyClass(this)
  }
  p3.await() // how to use myClass.scope here?
}
The context is GraphQL data fetchers where I set up the
MyClass
context in the entry method and then want to access the computed properties in some data fetcher, i.e. outside of the
runBlocking
scope.
d
It sounds like your trying to mix suspend/coroutine code with non-suspend code. Either all your code should be suspend, or you need to build a bridge (runBlocking is one such bridge).
This works for me:
Copy code
package com.stochastictinkr

import kotlinx.coroutines.*

class MyClass(scope: CoroutineScope) {
    val p1 by lazy {
        scope.async {
            println("start fetching p1")
            delay(1000)
            println("p1 done")

            3
        }
    }

    val p2 by lazy {
        scope.async {
            println("start fetching p2")
            delay(1000)
            println("p2 done")

            2
        }
    }

    val p3 by lazy {
        // Ensure that p1 and p2 are started before we await either of them.
        val p1 = this.p1
        val p2 = this.p2
        runBlocking {
            p1.await() + p2.await()
        }
    }

    val p4 by lazy {
        runBlocking {
            p1.await() * 3
        }
    }
}

fun main() {
    val obj = MyClass(CoroutineScope(Dispatchers.Default))
    println("p3: ${obj.p3}")
    println("p4: ${obj.p4}")
}
The output is:
Copy code
start fetching p1
start fetching p2
p1 done
p2 done
p3: 5
p4: 9
c
You definitely shouldn't use
runBlocking
in this situation, that undermines the entire point of coroutines (not cancellable, not compile-time declared, no structured concurrency)
d
They simply want to fetch two properties in parallel. If they wanted better/structured concurrency it would be a different design entirely.
c
If you're using coroutine scopes, you're very likely using
suspend
in other places in your application, meaning these properties will likely be accessed from deeper in the call stack than a
suspend
function, and using
runBlocking
there will break most guarantees offered by coroutines
d
@Daniel Pitts Interesting. So you can start an coroutine in one scope and wait for its completion in a different scope. That's what I was not aware of. And yes, you understood my use case correctly - I just want parallel computation, not structure concurrency.