https://kotlinlang.org logo
Title
d

Dmytro Danylyk

04/27/2018, 1:22 PM
Let me know if there is better way ⤴️
v

Vsevolod Tolstopyatov [JB]

04/27/2018, 3:20 PM
There is no better way for now. Currently, this a real issue with coroutines, one can easily forget to bind his jobs with Android activity, HTTP request lifecycle etc. and there is no way to enforce such binding. We have some thoughts about changing it: forbid top-level
async
and
launch
(and other top-level builders as well), define them as extensions on some parent job and provide an easy way to expose such parent job via android integration, new builders etc. The idea behind this approach is to make ownership of a job (actor, channel, cold stream, w/e) explicit in code, so cancellation will never be an issue. Then your example will be rewritten as
class MyFragment : Fragment() {
  fun loadData() = lifecycle.job().launch(UI) {} // <- job() is extension on lifecycle, won't compile if one tries to write fun loadData() = launch(UI) {}
}
Why do we want to do it if it will require more effort to write concurrent code (and break a lot of existing examples)? Consider the following snippet:
suspend fun computation() {
   val d1 = async {...}
   val d2 = async {...}
   return merge(d1.await(), d2.await())
}
If
d1
will throw an exception, no one will be able to cancel
d2
. And if
d2
throws another exception, no one will ever observe it! Though one may argue that it’s a programmer fault, this is how coroutines are usually recommended to be used (actually, it’s a simplified example from our guide). If
async
always should have a receiver,
computation
can be written only using some builder, e.g.:
suspend fun computation() = job {
   val d1 = async {...} // implicit receiver on `job {}` 
   val d2 = async {...}
   return merge(d1.await(), d2.await())
} // <- here builder will cancel launched tasks if necessary
Now all launched tasks will be properly cancelled/awaited in the end of
job {}
and it’s not possible to write
computation
without any builder. To launch jobs from non-suspendable contexts we will provide something like
Eternal.async {}
, where
Eternal
is
object
, so it always will be imported explicitly and code becomes more self-documented: “this job is launched via Eternal, so it can’t be cancelled as part of parent-child hierarchy and either it’s a mistake or it’s a system-wide job”. Opinions about this idea (note: it’s only a draft, not even prototyped) are welcome
d

Dmytro Danylyk

04/28/2018, 7:12 AM
Hmm that’s interesting approach. So, e.g. my typical android code would look like this:
val job = Job()
fun loadData() = job.launch(UI) {
    val result = withContext(CommonPool) { ... }
    view.showResult(result)
}
But, if I want to launch another coroutine how I can achieve that, in case if
async
require job receiver?
val job = Job()
fun loadData() = job.launch(UI) {
    // val result = async(CommonPool) { ... }.await()  can't do that, async require job receiver
    val result = job.async(CommonPool) { ... }.await() // this one is correct?
    view.showResult(result)
}
v

Vsevolod Tolstopyatov [JB]

04/28/2018, 8:25 AM
e.g. my typical android code would look like this
Empty jobs are not recommended, it doesn’t really make sense. Job should be bond with either your Android activity or another job.
But, if I want to launch another coroutine how I can achieve that
async
builder itself will have job (or something similar to it) as receiver, so nested async will work as expected:
val loadData() = job.launch(UI) {
   val innerTask = async(CommonPool) {}
   // Here inner task implicitly inherits job of UI activity and will be cancelled when activity is destroyed
}
d

Dmytro Danylyk

04/28/2018, 9:51 AM
Empty jobs are not recommended, it doesn’t really make sense.
I have made empty job to cancel it in activity
onDestroy
function. (because lifecycle aware stuff is available from library only, it’s not part of android sdk). With lifecycle callback it will look like in your sample
lifecycle.job().launch(UI)
.
Btw, it should not affect context switching right?
withContext
will stay the same correct?
v

Vsevolod Tolstopyatov [JB]

04/28/2018, 9:59 AM
Thanks for clarification, then your example is fine.
withContext
will stay correct, but this part of design is not yet properly investigated. Probably
withContext
should have job as well
d

Dmytro Danylyk

04/28/2018, 10:05 AM
If
withContext
need job as well, it would require write more code. E.g. currently in http client
fun loadUser(id: String, 
                       context: CoroutineContext = CommonPool) = withContext(context) {
    // logic
}
With new design:
fun loadUser(id: String, context: CoroutineContext = CommonPool, job: Job) 
= job.withContext(context) {
    // logic
}
I will have to pass job everywhere, am I right?
v

Vsevolod Tolstopyatov [JB]

04/28/2018, 10:48 AM
In such cases (when no one can cancel your task) you can use
Eternal
.
fun loadUser(id: String, context: CoroutineContext) = Eternal.withContext(context) {
    // logic
}
or maybe we can try to inherit job from given context (though it’s debatable approach)
d

dave08

04/29/2018, 4:56 AM
Doesn't a suspend fun have a coroutineContext...? withContext will only work in suspend fun...