Share
Embed
Kotlin Coroutines in Practice @ KotlinConf 2018
Let's see how Kotlin Coroutines are used to solve real-life concurrency and coordination problems. With coroutines we don't have to worry about shared mutable state and synchronization. We can solve the problems we face using a number of communicating coroutines, where each piece of state is confined to a single coroutine.
Roman Elizarov
October 05, 2018
Transcript
128 slides
-
suspend fun main() { val jobs = List(100_000) { GlobalScope.launch
{ delay(5000) print(".") } } jobs.forEach { it.join() } } Coroutines are like light-weight threads -
suspend fun main() { val jobs = List(100_000) { GlobalScope.launch
{ delay(5000) print(".") } } jobs.forEach { it.join() } } Coroutines are like light-weight threads -
suspend fun main() { val jobs = List(100_000) { GlobalScope.launch
{ delay(5000) print(".") } } jobs.forEach { it.join() } } Coroutines are like light-weight threads -
References Locations resolve fun processReferences(refs: List<Reference>) { for (ref in
refs) { val location = ref.resolveLocation() … } } -
References Locations Contents resolve download fun processReferences(refs: List<Reference>) { for
(ref in refs) { val location = ref.resolveLocation() val content = downloadContent(location) processContent(ref, content) } } -
References Locations Contents resolve download suspend fun processReferences(refs: List<Reference>) {
for (ref in refs) { val location = ref.resolveLocation() val content = downloadContent(location) processContent(ref, content) } } -
References Locations Contents resolve download suspend fun processReferences(refs: List<Reference>) {
for (ref in refs) { val location = ref.resolveLocation() val content = downloadContent(location) processContent(ref, content) } } Sequential -
References Locations Contents resolve download fun processReferences(refs: List<Reference>) { for
(ref in refs) { val location = ref.resolveLocation() val content = downloadContent(location) processContent(ref, content) } } Parallel -
References Locations Contents resolve download fun processReferences(refs: List<Reference>) { for
(ref in refs) { val location = ref.resolveLocation() GlobalScope.launch { val content = downloadContent(location) processContent(ref, content) } } } Parallel -
fun processReferences(refs: List<Reference>) { for (ref in refs) { val
location = ref.resolveLocation() GlobalScope.launch { val content = downloadContent(location) processContent(ref, content) } } } Coroutines are cheap! What could go wrong? -
fun processReferences(refs: List<Reference>) { for (ref in refs) { val
location = ref.resolveLocation() GlobalScope.launch { val content = downloadContent(location) processContent(ref, content) } } } ref1 location1 Launch download ref2 location2 Launch download ref3 Crash! Crash! -
ref1 location1 Launch download ref2 location2 Launch download ref3 Crash!
Crash! Leak! fun processReferences(refs: List<Reference>) { for (ref in refs) { val location = ref.resolveLocation() GlobalScope.launch { val content = downloadContent(location) processContent(ref, content) } } } -
suspend fun processReferences(refs: List<Reference>) = coroutineScope { for (ref in
refs) { val location = ref.resolveLocation() GlobalScope.launch { val content = downloadContent(location) processContent(ref, content) } } } -
suspend fun processReferences(refs: List<Reference>) = coroutineScope { for (ref in
refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } } -
suspend fun processReferences(refs: List<Reference>) = coroutineScope { for (ref in
refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } } Child -
Crash? suspend fun processReferences(refs: List<Reference>) = coroutineScope { for (ref
in refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } } -
suspend fun processReferences(refs: List<Reference>) = coroutineScope { for (ref in
refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } } Crash? -
suspend fun processReferences(refs: List<Reference>) = coroutineScope { for (ref in
refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } } Crash? cancels -
Crash? cancels Waits for completion suspend fun processReferences(refs: List<Reference>) =
coroutineScope { for (ref in refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } } -
suspend fun processReferences(refs: List<Reference>) = coroutineScope { for (ref in
refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } } Never leaks jobs -
class Downloader { private val requested = mutableSetOf<Location>() fun downloadReference(ref:
Reference) { val location = ref.resolveLocation() … } } -
class Downloader { private val requested = mutableSetOf<Location>() fun downloadReference(ref:
Reference) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } } } -
class Downloader { private val requested = mutableSetOf<Location>() fun downloadReference(ref:
Reference) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } -
class Downloader { private val requested = mutableSetOf<Location>() fun downloadReference(ref:
Reference) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } Concurrent -
class Downloader { private val requested = mutableSetOf<Location>() fun downloadReference(ref:
Reference) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } Concurrent -
class Downloader { private val requested = mutableSetOf<Location>() fun downloadReference(ref:
Reference) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } Shared mutable state -
class Downloader { private val requested = mutableSetOf<Location>() fun downloadReference(ref:
Reference) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } Shared mutable state Needs Synchronization Shared + Mutable = -
launch { val requested = mutableSetOf<Location>() … } Does not
share mutable state -
launch { val requested = mutableSetOf<Location>() for (ref in references)
{ val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } -
launch { val requested = mutableSetOf<Location>() for (ref in references)
{ val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } -
fun CoroutineScope.downloader( references: ReceiveChannel<Reference>, ) = launch { val requested
= mutableSetOf<Location>() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } -
fun CoroutineScope.downloader( references: ReceiveChannel<Reference>, ) = launch { val requested
= mutableSetOf<Location>() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } -
fun CoroutineScope.downloader( references: ReceiveChannel<Reference>, ) = launch { val requested
= mutableSetOf<Location>() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } -
fun CoroutineScope.downloader( references: ReceiveChannel<Reference>, ) = launch { val requested
= mutableSetOf<Location>() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } -
fun CoroutineScope.downloader( references: ReceiveChannel<Reference>, ) = launch { val requested
= mutableSetOf<Location>() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { launch { … } } // ... wait for result ... processContent(ref, content) } } Coroutines are cheap! What could go wrong? -
fun CoroutineScope.downloader( references: ReceiveChannel<Reference>, ) = launch { val requested
= mutableSetOf<Location>() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { launch { … } } // ... wait for result ... processContent(ref, content) } } Child -
fun CoroutineScope.downloader( references: ReceiveChannel<Reference>, ) = launch { val requested
= mutableSetOf<Location>() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { launch { … } } // ... wait for result ... processContent(ref, content) } } Coroutines are cheap! But the work they do… -
Worker 1 Worker 2 Worker 3 Worker N Worker pool
… References Downloader references locations -
fun CoroutineScope.downloader( references: ReceiveChannel<Reference>, locations: SendChannel<Location> ) = launch {
val requested = mutableSetOf<Location>() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { locations.send(location) } } } -
fun CoroutineScope.worker( locations: ReceiveChannel<Location> ) = launch { for (loc
in locations) { val content = downloadContent(loc) processContent(ref, content) } } -
fun CoroutineScope.worker( locations: ReceiveChannel<Location> ) = launch { for (loc
in locations) { val content = downloadContent(loc) processContent(ref, content) } } Fan-out -
fun CoroutineScope.worker( locations: ReceiveChannel<Location> ) = launch { for (loc
in locations) { val content = downloadContent(location) processContent(ref, content) } } -
fun CoroutineScope.worker( locations: ReceiveChannel<Location> ) = launch { for (loc
in locations) { val content = downloadContent(loc) processContent(ref, content) } } -
fun CoroutineScope.worker( locations: ReceiveChannel<Location> ) = launch { for (loc
in locations) { val content = downloadContent(loc) processContent(ref, content) } } -
Worker 1 Worker 2 Worker 3 Worker N … References
Downloader Refs ↔ Locs location & content -
data class LocContent(val loc: Location, val content: Content) fun CoroutineScope.worker(
locations: ReceiveChannel<Location>, contents: SendChannel<LocContent> ) -
data class LocContent(val loc: Location, val content: Content) fun CoroutineScope.worker(
locations: ReceiveChannel<Location>, contents: SendChannel<LocContent> ) = launch { for (loc in locations) { val content = downloadContent(loc) contents.send(LocContent(loc, content)) } } -
Worker 1 Worker 2 Worker 3 Worker N … References
Downloader locations contents -
fun CoroutineScope.downloader( references: ReceiveChannel<Reference>, locations: SendChannel<Location>, contents: ReceiveChannel<LocContent> ) =
launch { val requested = mutableSetOf<Location>() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { locations.send(location) } } } Hmm…. -
select { references.onReceive { ref -> … } contents.onReceive {
(loc, content) -> … } } -
select { references.onReceive { ref -> … } contents.onReceive {
(loc, content) -> … } } -
select<Unit> { references.onReceive { ref -> … } contents.onReceive {
(loc, content) -> … } } -
launch { val requested = mutableMapOf<Location, MutableList<Reference>>() while (true) {
select<Unit> { references.onReceive { ref -> … } contents.onReceive { (loc, content) -> … } } } } -
launch { val requested = mutableMapOf<Location, MutableList<Reference>>() while (true) {
select<Unit> { references.onReceive { ref -> … } contents.onReceive { (loc, content) -> … } } } } -
launch { val requested = mutableMapOf<Location, MutableList<Reference>>() while (true) {
select<Unit> { references.onReceive { ref -> val loc = ref.resolveLocation() … } contents.onReceive { (loc, content) -> … } } } } -
launch { val requested = mutableMapOf<Location, MutableList<Reference>>() while (true) {
select<Unit> { references.onReceive { ref -> val loc = ref.resolveLocation() val refs = requested[loc] … } contents.onReceive { (loc, content) -> … } } } } -
launch { val requested = mutableMapOf<Location, MutableList<Reference>>() while (true) {
select<Unit> { references.onReceive { ref -> val loc = ref.resolveLocation() val refs = requested[loc] if (refs == null) { requested[loc] = mutableListOf(ref) locations.send(loc) } } contents.onReceive { (loc, content) -> … } } } } -
launch { val requested = mutableMapOf<Location, MutableList<Reference>>() while (true) {
select<Unit> { references.onReceive { ref -> val loc = ref.resolveLocation() val refs = requested[loc] if (refs == null) { requested[loc] = mutableListOf(ref) locations.send(loc) } else { refs.add(ref) } } contents.onReceive { (loc, content) -> … } } } } -
launch { val requested = mutableMapOf<Location, MutableList<Reference>>() while (true) {
select<Unit> { references.onReceive { ref -> val loc = ref.resolveLocation() val refs = requested[loc] if (refs == null) { requested[loc] = mutableListOf(ref) locations.send(loc) } else { refs.add(ref) } } contents.onReceive { (loc, content) -> … } } } } No concurrency No synchronization -
launch { val requested = mutableMapOf<Location, MutableList<Reference>>() while (true) {
select<Unit> { references.onReceive { ref -> … } contents.onReceive { (loc, content) -> val refs = requested.remove(loc)!! for (ref in refs) { processContent(ref, content) } } } } } -
Worker 1 Worker 2 Worker 3 Worker N … References
Downloader locations contents references -
Worker 1 Worker 2 Worker 3 Worker N … References
Downloader locations contents references -
fun CoroutineScope.processReferences( references: ReceiveChannel<Reference> ) { val locations = Channel<Location>()
val contents = Channel<LocContent>() repeat(N_WORKERS) { worker(locations, contents) } downloader(references, locations, contents) } -
fun CoroutineScope.processReferences( references: ReceiveChannel<Reference> ) { val locations = Channel<Location>()
val contents = Channel<LocContent>() repeat(N_WORKERS) { worker(locations, contents) } downloader(references, locations, contents) } -
Worker 1 Worker 2 Worker 3 Worker N … References
Downloader locations contents references -
Worker 1 Worker 2 Worker 3 Worker N … References
Downloader locations contents references processReferences -
Worker 1 Worker 2 Worker 3 Worker N … References
Downloader locations contents references processReferences Patterns everywhere Worker pool Actor -
class SomethingWithLifecycle : CoroutineScope { private val job = Job()
override val coroutineContext: CoroutineContext get() = … } -
class SomethingWithLifecycle : CoroutineScope { private val job = Job()
fun dispose() { … } override val coroutineContext: CoroutineContext get() = … } -
class SomethingWithLifecycle : CoroutineScope { private val job = Job()
fun close() { … } override val coroutineContext: CoroutineContext get() = … } -
class SomethingWithLifecycle : CoroutineScope { private val job = Job()
fun close() { job.cancel() } override val coroutineContext: CoroutineContext get() = … } -
class SomethingWithLifecycle : CoroutineScope { private val job = Job()
fun close() { job.cancel() } override val coroutineContext: CoroutineContext get() = job } -
class SomethingWithLifecycle : CoroutineScope { private val job = Job()
fun close() { job.cancel() } override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main } -
class SomethingWithLifecycle : CoroutineScope { … override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main fun doSomething() { } } -
class SomethingWithLifecycle : CoroutineScope { … override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main fun doSomething() { launch { … } } } -
class SomethingWithLifecycle : CoroutineScope { … override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main fun doSomething() { processReferences(references) } } Never leak any coroutines -
suspend fun downloadContent(location: Location): Content Does something long & waits
for it to complete without blocking -
suspend fun downloadContent(location: Location): Content fun CoroutineScope.processReferences(…) Launches new coroutines
& quickly returns, does not wait for them