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

Dissecting Coroutines

Mohit S
January 16, 2019

Dissecting Coroutines

2019 maybe be the year of Kotlin coroutines. What is a coroutine anyway? A coroutine is an instance of a state machine and a suspending function defines the state machine. What does that mean? Join me in this talk as we will explore the ins and out of coroutines. We will dive deep into channels, actors and the reactive streams modules. We’ll also look at their usage to solve common async problems in Android.

Mohit S

January 16, 2019
Tweet

More Decks by Mohit S

Other Decks in Programming

Transcript

  1. Dissecting Coroutines • Making API requests • Structured Concurrency •

    Under the hood • Context & Dispatchers • Using coroutines on Android • Channels • Shared mutable state
  2. sealed class ApiResponse<out T> { class Success<out T>(val data: T):

    ApiResponse<T>() class Error(val t: Throwable): ApiResponse<Nothing>() }
  3. suspend fun login(email: String, password: String): ApiResponse<User> { } Suspending

    Function • Stop & yield without blocking calling thread • Resume execution when ready
  4. suspend fun login(email: String, password: String): ApiResponse<User> { val call

    = apiService.login(email, password) } How do we suspend and resume with the result?
  5. suspend fun <T : Any> Call<T>.await(): Response<T> { enqueue(object :

    Callback<T> { 
 fun onResponse(call: Call<T>, resp: Response<T>) { 
 } fun onFailure(call: Call<T>, t: Throwable) { } }) }
  6. suspend fun <T : Any> Call<T>.await(): Response<T> { suspendCancellableCoroutine {

    cont !-> 
 enqueue(object : Callback<T> { 
 fun onResponse(c: Call<T>, resp: Response<T>) { } fun onFailure(c: Call<T>, t: Throwable) { } }) } }
  7. suspend fun <T : Any> Call<T>.await(): Response<T> { suspendCancellableCoroutine {

    cont !-> 
 enqueue(object : Callback<T> { 
 fun onResponse(c: Call<T>, resp: Response<T>) { if (resp.isSuccessful) { cont.resume(resp) } else { cont.resumeWithException(HttpException(resp)) } } … }) } }
  8. suspend fun <T : Any> Call<T>.await(): Response<T> { suspendCancellableCoroutine {

    cont !-> 
 enqueue(object : Callback<T> { 
 … fun onFailure(c: Call<T>, t: Throwable) { cont.resumeWithException(t) } }) } }
  9. suspend fun <T : Any> Call<T>.await(): Response<T> { return suspendCancellableCoroutine

    { cont !-> enqueue(object : Callback<T> { override fun onResponse(c: Call<T>, resp: Response<T>) { if (resp.isSuccessful) { cont.resume(resp) } else { cont.resumeWithException(HttpException(resp)) } } override fun onFailure(c: Call<T>, t: Throwable) { cont.resumeWithException(t) } }) } }
  10. GlobalScope.launch { val response = login(email, password) when(response) { is

    ApiResponse.Success !-> { } is ApiResponse.Error !-> { } } }
  11. GlobalScope.launch { val response = login(email, password) when(response) { is

    ApiResponse.Success !-> { } is ApiResponse.Error !-> { } } } Scope to lifecycle of app
  12. val job: Job = GlobalScope.launch { val response = login(email,

    password) when (response) { is ApiResponse.Success !-> { } is ApiResponse.Error !-> { } } }
  13. val job: Job = GlobalScope.launch { val response = login(email,

    password) when (response) { is ApiResponse.Success !-> { } is ApiResponse.Error !-> { } } } job.cancel()
  14. fun authenticate(email: String, password: String, callback: Callback) { login(email, password)

    { user !-> token(user.id) { token !-> storeToken(token) { callback.success() } } } } Chained callbacks
  15. fun authenticate(email: String, password: String) { GlobalScope.launch { val user

    = login(email, password) val token = getToken(user.id) storeToken(token) } }
  16. fun authenticate(email: String, password: String) { GlobalScope.launch { val user

    = login(email, password) val token = getToken(user.id) storeToken(token) } } How do I return the result to the caller?
  17. fun authenticate(email: String, password: String): Deferred<Boolean> = GlobalScope.async { val

    user = login(email, password) val token = getToken(user.id) storeToken(token) }
  18. fun showPostDetails(posts: List<Post>) { for (post in posts) { GlobalScope.launch

    { val mediaItem = loadMediaItems(post.id) showMediaItem(mediaItem) } } }
  19. fun showPostDetails(posts: List<Post>) { for (post in posts) { GlobalScope.launch

    { val mediaItem = loadMediaItems(post.id) showMediaItem(mediaItem) } } } Request 1 Request 2 Request 3 Request 4
  20. fun showPostDetails(posts: List<Post>) { for (post in posts) { GlobalScope.launch

    { val mediaItem = loadMediaItems(post.id) showMediaItem(mediaItem) } } } Request 1 Request 2 Request 3 Request 4
  21. suspend fun showPostDetails(posts: List<Post>) { for (post in posts) {

    GlobalScope.launch { val mediaItem = loadMediaItems(post.id) showMediaItem(mediaItem) } } } Suspending method
  22. suspend fun showPostDetails(posts: List<Post>) { coroutineScope { for (post in

    posts) { GlobalScope.launch { val mediaItem = loadMediaItems(post.id) showMediaItem(mediaItem) } } } } Create a scope
  23. suspend fun showPostDetails(posts: List<Post>) { coroutineScope { for (post in

    posts) { launch { val mediaItem = loadMediaItems(post.id) showMediaItem(mediaItem) } } } } Request 1 Request 2 Request 3 Request 4 Cancel all the coroutines
  24. GlobalScope.launch { val user = login(email, password) val token =

    getToken(user.id) storeToken(token) } Create an instance of state machine
  25. Continuation create(…) { int label; Object invokeSuspend() { switch(this.label) {

    case 0: … case 1: … case 2: … } } } login(email, password) getToken(user.id) storeToken(token)
  26. class CancellableContinuationImpl<in T> : CancellableContinuation<T> { val _state = atomic<Any?>(Active)

    
 fun resumeWith(result: Result<T>) 
 fun invokeSuspend() 
 fun cancel(cause: Throwable?): Boolean }
  27. switch(this.label) { case 0: CoroutineScope var2 = this.p$; var6 =

    MainActivity.this.getEmail(); String var10001 = MainActivity.this.getPassword(); this.label = 1; var10000 = MainActivityKt.login(var6, var10001, this); break; Save suspension point
  28. switch(this.label) { case 0: CoroutineScope var2 = this.p$; var6 =

    MainActivity.this.getEmail(); String var10001 = MainActivity.this.getPassword(); this.label = 1; var10000 = MainActivityKt.login(var6, var10001, this); break; Pass in instance of state machine
  29. interface CoroutineContext { fun <E : Element> get(key: Key<E>): E?

    fun plus(context: CoroutineContext): CoroutineContext … }
  30. class LoginRepository(val dispatcher: CoroutineDispatcher, val apiService: ApiService) { suspend fun

    login(email: String, password: String): ApiResponse<User> = withContext(dispatcher) { try { val response = apiService.login(email, password).await() ApiResponse.Success(response.body()) } catch (e: HttpException) { ApiResponse.Error(e) } } }
  31. class LoginViewModel(val loginRepository: LoginRepository): ViewModel() { val viewModelJob = Job()

    val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) }
  32. class LoginViewModel(val loginRepository: LoginRepository): ViewModel() { val uiScope = CoroutineScope(Dispatchers.Main

    + viewModelJob) fun onLoginClicked(email: String, password: String) { uiScope.launch { val apiResponse = loginRepository.login(email, password) } } … }
  33. class LoginViewModel(val loginRepository: LoginRepository): ViewModel() { val uiScope = CoroutineScope(Dispatchers.Main

    + viewModelJob) fun onLoginClicked(email: String, password: String) { uiScope.launch { val apiResponse = loginRepository.login(email, password) when(apiResponse) { is ApiResponse.Success !-> { showActivity() } is ApiResponse.Error !-> { showError() } } } } }
  34. class LoginViewModel(val loginRepository: LoginRepository): ViewModel() { val viewModelJob = Job()

    … override fun onCleared() { super.onCleared() viewModelJob.cancel() } }
  35. val ViewModel.viewModelScope: CoroutineScope get() { val scope: CoroutineScope? = this.getTag(JOB_KEY)

    if (scope !!= null) { return scope } return setTagIfAbsent(JOB_KEY, CloseableCoroutineScope(Job() + Dispatchers.Main)) }
  36. class LoginViewModel(val loginRepository: LoginRepository): ViewModel() { fun onLoginClicked(email: String, password:

    String) { viewModelScope.launch { val apiResponse = loginRepository.login(email, password) } } }
  37. val channel = Channel<Int>() launch { for (x in 1!..5)

    channel.send(x * x) } 1 4 9 16 25
  38. val channel = Channel<Int>() launch { for (x in 1!..5)

    channel.send(x * x) } repeat(5) { println(channel.receive()) } 1 4 9 16 25 Producer Consumer
  39. val numbers = produceNumbers() val squares = square(numbers) for (i

    in 1!..5) println(squares.receive()) coroutineContext.cancelChildren() Composing channels
  40. val numbers = produceNumbers() val squares = square(numbers) for (i

    in 1!..5) println(squares.receive()) for (i in 1!..5) println(squares.receive()) coroutineContext.cancelChildren() Cannot consume channel again.
  41. val numbers = produceNumbers() val squares = square(numbers) for (i

    in 1!..5) println(squares.receive()) for (i in 1!..5) println(squares.receive()) coroutineContext.cancelChildren()
  42. var counter = 0 suspend fun CoroutineScope.massiveRun(action: suspend () !->

    Unit) { val jobs = List(100) { launch { } } } Launch 100 coroutines!
  43. var counter = 0 suspend fun CoroutineScope.massiveRun(action: suspend () !->

    Unit) { val jobs = List(100) { launch { repeat(1000) { action() } } } } Mutate counter 1000 times!
  44. public interface Mutex { public val isLocked: Boolean public suspend

    fun lock(owner: Any? = null) public fun unlock(owner: Any? = null) … }
  45. var mutext = Mutex() var counter = 0 GlobalScope.massiveRun {

    mutext.lock() try { counter!++ } finally { mutext.unlock() } } Critical Section
  46. public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action:

    () !-> T ): T { lock(owner) try { return action() } finally { unlock(owner) } }
  47. sealed class CounterEvents object IncCounter : CounterEvents() class GetCounter( val

    response: CompletableDeferred<Int> ) : CounterEvents() Channel Coroutine GetCounter
  48. fun CoroutineScope.counterActor() = actor<CounterEvents> { var counter = 0 for

    (msg in channel) { when (msg) { is IncCounter !-> counter!++ is GetCounter !-> msg.response.complete(counter) } } } Process Events
  49. val actor = counterActor() GlobalScope.massiveRun { actor.send(IncCounter) } val response

    = CompletableDeferred<Int>() counter.send(GetCounter(response)) println(response.await()) counter.close() Send Message 
 to get value