Dmytro Danylyk
04/27/2018, 1:22 PMVsevolod Tolstopyatov [JB]
04/27/2018, 3:20 PMasync
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 welcomeDmytro Danylyk
04/28/2018, 7:12 AMval job = Job()
fun loadData() = job.launch(UI) {
val result = withContext(CommonPool) { ... }
view.showResult(result)
}
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)
}
Vsevolod Tolstopyatov [JB]
04/28/2018, 8:25 AMe.g. my typical android code would look like thisEmpty 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
}
Dmytro Danylyk
04/28/2018, 9:51 AMEmpty 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)
.withContext
will stay the same correct?Vsevolod Tolstopyatov [JB]
04/28/2018, 9:59 AMwithContext
will stay correct, but this part of design is not yet properly investigated. Probably withContext
should have job as wellDmytro Danylyk
04/28/2018, 10:05 AMwithContext
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?Vsevolod Tolstopyatov [JB]
04/28/2018, 10:48 AMEternal
.
fun loadUser(id: String, context: CoroutineContext) = Eternal.withContext(context) {
// logic
}
dave08
04/29/2018, 4:56 AM