Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin/Native + Coroutines

Avatar for Ben Asher Ben Asher
August 27, 2019

Kotlin/Native + Coroutines

An overview of how concurrency and mutability work in Kotlin/Native and strategies for how to get multi-threaded work done in that world using coroutines.

Avatar for Ben Asher

Ben Asher

August 27, 2019

More Decks by Ben Asher

Other Decks in Programming

Transcript

  1. PlanGrid by Autodesk • Works offline (construction sites have bad

    internet) • Over time, sync system rots in different ways on each platform • Fixed cost to build features
  2. PlanGrid by Autodesk • Multiplatform shared sync • JSON: kotlinx.serialization

    • sqlite: SQLDelight Sync Coordination Sheets Annotations More… Task JSON sqlite JSON sqlite JSON sqlite Work for each task kotlinx.coroutines
  3. State of Coroutines • CoroutineDispatcher: determines how blocks of code

    are run • Dispatchers.Default: background pool • JVM: works as expected ✅ Thread A Thread B Thread C {coroutine code} CoroutineDispatcher
  4. Native: “There is no event loop. Use runBlocking…” class SyncWork:

    CoroutineScope { override val coroutineContext: CoroutineContext = Job() •Starts an event loop •Blocks current thread •Confined to that thread fun start() { launch { // SYNC } } fun stop() { coroutineContext.cancel() } } fun doSync() { runBlocking { // SYNC } }
  5. CoroutineWorker // executes a the block in a worker pool

    fun execute(block: suspend CoroutineScope.() -> Unit) // performs the block on another thread // and suspends, waiting for the result suspend fun <T> performAndWait(block: suspend CoroutineScope.() -> T): T
  6. Kotlin/Native + Coroutines Today: build pieces of autodesk/CoroutineWorker • Mutability

    and concurrency rules in K/N • execute: coroutine worker pool • performAndWait: Doing work across threads Say Mean lambda lambda closure lambda block lambda
  7. Immutable .freeze() Mutability & Concurrency • When an object is

    created, it’s “owned” by that thread • Accesses for that object are allowed on that thread only • 2 ways to move objects across threads Transfer Thread 1 Thread 2 SomeObj SomeObj Freeze
  8. Mutability & Concurrency kotlin.IllegalStateException: Illegal transfer state +1 ref count

    here • Worker API (read “worker thread”) • execute transfers object through producer to perform work val addThese = Pair(1, 2) worker.execute(TransferMode.SAFE, { addThese }) { println("bg result: ${it.first + it.second}") }
  9. Mutability & Concurrency Fix 1: Properly transfer • Uninspiring •

    How do I scale creating objects inside a producer block? • Typical background work use cases are more complex worker.execute(TransferMode.SAFE, { Pair(1, 2) }) { println("bg result: ${it.first + it.second}") }
  10. Mutability & Concurrency Fix 2: Freeze • Pair is immutable

    anyway (in this case) • Simplest approach in my view • We can more easily adopt our mental model to program this way val addThese = Pair(1, 2).freeze() worker.execute(TransferMode.SAFE, { addThese }) { println("bg result: ${it.first + it.second}") }
  11. Mutability & Concurrency • Works well for background work that

    produce a result • Works well for sync Network Data Wire types DB types sqlite Not specific to sync
  12. Mutability & Concurrency • Watch out: freeze large object graph

    by mistake • Use .ensureNeverFrozen() Save yourself: init{ this.ensureNeverFrozen() } class Sample { val addThese = Pair(1, 2) Implicit this fun doWork() { val work = { println("bg result: ${addThese.first + addThese.second}") }.freeze() // do something with work } }
  13. Mutability & Concurrency class Sample { val addThese = Pair(1,

    2) fun doWork() { val local = this.addThese val work = { println("bg result: ${local.first + local.second}”) }.freeze() } } Fix: local reference
  14. Kotlin/Native + Coroutines Today: build pieces of autodesk/CoroutineWorker • Mutability

    and concurrency rules in K/N ✅ • execute: coroutine worker pool • performAndWait: do work across threads
  15. Coroutines + Worker Pool • What about CoroutineDispatcher? Native package

    kotlinx.coroutines public abstract class CoroutineDispatcher
  16. Coroutines + Worker Pool • What to do with the

    block? • Transfer it? • Freeze? • Sad answer: have to wait for magic that will fix #462
  17. • Right now: • Can’t switch between threads inside of

    coroutine code • CAN pick a thread before running the coroutine code Coroutines + Worker Pool Native: coroutines require runBlocking fun doSync() { runBlocking { // SYNC } }
  18. Coroutines + Worker Pool Step 1 worker.execute(TransferMode.SAFE, { }) {

    } runBlocking { // coroutine on background thread }
  19. worker.execute(TransferMode.SAFE, { work.freeze() }) { runBlocking { it() } }

    } fun performWork(work: suspend CoroutineScope.() -> Unit) { • Wrap in an API for running an arbitrary block in the background Coroutines + Worker Pool Step 2 Freeze the work closure Final step: add more workers!
  20. class WorkerPool { private val workers: List<Worker> = (0..5).map {

    Worker.start() } private val workerIndex = AtomicInt(0) init { freeze() } fun performWork(work: suspend CoroutineScope.() -> Unit) { // pick a worker val nextWorkerIndex = workerIndex.addAndGet(1) val worker = workers[nextWorkerIndex % workers.count()] worker.execute(TransferMode.SAFE, { work.freeze() }) { runBlocking { it() } } } }
  21. Kotlin/Native + Coroutines Today: build pieces of autodesk/CoroutineWorker • Mutability

    and concurrency rules in K/N ✅ • execute: coroutine-worker pool ✅ • performAndWait: do work across threads
  22. Doing Work Across • Tools to get there: • AtomicReference<T>:

    holds a reference to a frozen object • Can freeze it • Can update it across threads package kotlin.native.concurrent public class AtomicReference<T>(private var value_: T) { // updates the reference public var value: T
  23. • Tools to get there: • StableRef<T: Any> Doing Work

    Across val ref = StableRef.create(someMutableObject).freeze() // sometime later val unpackedRef = ref.get() originalRef.dispose()
  24. • Tools to get there: • AtomicInt (also AtomicLong) Doing

    Work Across package kotlin.native.concurrent public class AtomicInt(private var value_: Int) { public var value: Int public fun addAndGet(delta: Int): Int
  25. • We have the tools Doing Work Across suspend fun

    <T> performAndWait(work: suspend CoroutineScope.() -> T): T Thread A parsing json doing something else performAndWait { // write to sqlite } Thread B write to sqlite Worker pool
  26. Doing Work Across suspend fun <T> performAndWait(work: suspend CoroutineScope.() ->

    T): T { // suspend the current coroutine while the work happens suspendCoroutine<T> { continuation -> // start the work somewhere in the pool pool.performWork { // perform the work, capturing error or success val result = runCatching { work() } // what to do with result? } } }
  27. • Options: • Use a regular Future? (blocking) • Coroutines

    Mutex? (#462) Doing Work Across suspend fun <T> performAndWait(work: suspend CoroutineScope.() -> T): T { suspendCoroutine<T> { continuation -> pool.performWork { val result = runCatching { work() } // what to do with result? } } }
  28. • Solution: delay(50) + AtomicReference • Keep a local AtomicReference

    (safe to freeze) • In a another coroutine:
 while(result.value == null) { delay(50) } Doing Work Across suspend fun <T> performAndWait(work: suspend CoroutineScope.() -> T): T { suspendCoroutine<T> { continuation -> pool.performWork { val result = runCatching { work() } // what to do with result? } } }
  29. Doing Work Across suspend fun <T> performAndWait(work: suspend CoroutineScope.() ->

    T): T { val resultRef = AtomicReference<Result<T>?>(null) launch { while (resultRef.value == null) { delay(50) } // cross-thread work complete } suspendCoroutine<T> { continuation -> pool.performWork { val result = runCatching { work() } resultRef.value = result.freeze() } } } Update result on complete How do we resume?
  30. StableRef! suspend fun <T> performAndWait(work: suspend CoroutineScope.() -> T): T

    { val resultRef = AtomicReference<Pair<Result<T>, StableRef<Continuation<T>>>?>(null) suspendCoroutine<T> { continuation -> val continuationRef = StableRef.create(continuation) pool.performWork { val result = runCatching { work() } resultRef.value = Pair(result, continuationRef).freeze() } } } Hide in StableRef launch { while (resultRef.value == null) { delay(50) } // cross-thread work complete // unpack result and continuation val (result, continuationRef) = resultRef.value!! val continuation = continuationRef.get() // resume continuation.resumeWith(result) } Unpack
  31. • Basics of CoroutineWorker execute and performAndWait • CoroutineWorker handles

    more (no time) • Cancellation of work • Better scheduling / load-balancing in pool • Avoids leaks with AtomicReference Doing Work Across
  32. • K/N concurrency model • Strategies for working with frozen

    state (or can’t be frozen) • AtomicReference • StableRef • Understand the basics of CoroutineWorker • IMO, best we can do on native until #462 Review