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

Kotlin Coroutines: Beyond async-await

bolot
August 27, 2018

Kotlin Coroutines: Beyond async-await

Kotlin coroutines support the familiar async/await style, as well as channel and goroutine styles. Learning from the predecessors, designers of the language managed to accomplish this with only one reserved keyword: suspend.

Coroutines were invented at the dawn of programming languages, but were quickly forgotten when threads and other fashionable techniques were popularized. They are making a major comeback as many mainstream languages have introduced support for coroutines and similar mechanisms: async/await, channels, goroutines. Kotlin language designers figured out a way to emulate many of those patterns by pushing the details to the library functions and reserving only one keyword: suspend.

bolot

August 27, 2018
Tweet

More Decks by bolot

Other Decks in Programming

Transcript

  1. Problem val image1 = loadImage(url1) val image2 = loadImage(url2) val

    result = composeImages(image1, image2) displayImage(result)
  2. Callback Hell peerConnection.setRemoteDescription(object: SdpObserver { override fun onSetFailure(error: String?) {

    } override fun onSetSuccess() { peerConnection.createAnswer(object : SdpObserver { override fun onSetFailure(error: String?) { } override fun onSetSuccess() { } override fun onCreateSuccess(sdp: SessionDescription?) { peerConnection.setLocalDescription(object : SdpObserver { override fun onSetFailure(error: String?) { } override fun onSetSuccess() { } override fun onCreateSuccess(sdp: SessionDescription?) { // TODO: get audio/video tracks } override fun onCreateFailure(error: String?) { } }, sdp) } override fun onCreateFailure(error: String?) { } }, mediaConstraints) } override fun onCreateSuccess(sdp: SessionDescription?) { } override fun onCreateFailure(error: String?) { } }, sessionDescription)
  3. Coroutines val image1 = async { loadImage(url1) } val image2

    = async { loadImage(url2) } val result = composeImages(image1.await(), image2.await())
  4. async/await val image1 = async { loadImage(url1) } val image2

    = async { loadImage(url2) } val result = composeImages(image1.await(), image2.await())
  5. Behind the Scenes val image1 = loadImage(url1) val image2 =

    loadImage(url2) val result = composeImages(image1, image2) displayImage(result)
  6. Behind the Scenes class CoroutinesReversed { class Continuation(val label: Int,

    val context: Map<String, Any?>, val error: Throwable?) fun load(cont: Continuation) { when (cont.label) { 0 -> { cont.error?.let { throw it } val url1 = cont.context["url1"] as String val image1 = loadImage(url1) load(Continuation(1, cont.context.plus("image1" to image1), null)) } 1 -> { cont.error?.let { throw it } val url2 = cont.context["url2"] as String val image2 = loadImage(url2) load(Continuation(2, cont.context.plus("image2" to image2), null)) } 2 -> { cont.error?.let { throw it } val image1 = cont.context["image1"] as Image val image2 = cont.context["image2"] as Image val result = composeImages(image1, image2) load(Continuation(3, cont.context.plus("result" to result), null)) } 3 -> { val result = cont.context["result"] as Image displayImage(result) } } } fun load() = load(Continuation(0, emptyMap(), null)) }
  7. Behind the Scenes class CoroutinesReversed { class Continuation(val label: Int,

    val context: Map<String, Any?>, val error: Throwable?) fun load(cont: Continuation) { when (cont.label) { 0 -> { cont.error?.let { throw it } val url1 = cont.context["url1"] as String val image1 = loadImage(url1) load(Continuation(1, cont.context.plus("image1" to image1), null)) } 1 -> { cont.error?.let { throw it } val url2 = cont.context["url2"] as String val image2 = loadImage(url2) load(Continuation(2, cont.context.plus("image2" to image2), null)) } 2 -> { cont.error?.let { throw it } val image1 = cont.context["image1"] as Image val image2 = cont.context["image2"] as Image val result = composeImages(image1, image2) load(Continuation(3, cont.context.plus("result" to result), null)) } 3 -> { val result = cont.context["result"] as Image displayImage(result) } } } fun load() = load(Continuation(0, emptyMap(), null)) }
  8. Behind the Scenes class CoroutinesReversed { class Continuation(val label: Int,

    val context: Map<String, Any?>, val error: Throwable?) fun load(cont: Continuation) { when (cont.label) { 0 -> { cont.error?.let { throw it } val url1 = cont.context["url1"] as String val image1 = loadImage(url1) load(Continuation(1, cont.context.plus("image1" to image1), null)) } 1 -> { cont.error?.let { throw it } val url2 = cont.context["url2"] as String val image2 = loadImage(url2) load(Continuation(2, cont.context.plus("image2" to image2), null)) } 2 -> { cont.error?.let { throw it } val image1 = cont.context["image1"] as Image val image2 = cont.context["image2"] as Image val result = composeImages(image1, image2) load(Continuation(3, cont.context.plus("result" to result), null)) } 3 -> { val result = cont.context["result"] as Image displayImage(result) } } } fun load() = load(Continuation(0, emptyMap(), null)) }
  9. Behind the Scenes class Continuation(val label: Int, val context: Map<String,

    Any?>, val error: Throwable?) 2 -> { cont.error?.let { throw it } val image1 = cont.context["image1"] as Image val image2 = cont.context["image2"] as Image val result = composeImages(image1, image2) load(Continuation(3, cont.context.plus("result" to result), null)) }
  10. Behind the Scenes class CoroutinesReversed { class Continuation(val label: Int,

    val context: Map<String, Any?>, val error: Throwable?) fun load(cont: Continuation) { when (cont.label) { 0 -> { cont.error?.let { throw it } val url1 = cont.context["url1"] as String val image = loadImage(url1) load(Continuation(1, cont.context.plus("image1" to image), null)) } 1 -> { cont.error?.let { throw it } val url2 = cont.context["url2"] as String val image = loadImage(url2) load(Continuation(2, cont.context.plus("image2" to image), null)) } 2 -> { cont.error?.let { throw it } val image1 = cont.context["image1"] as Image val image2 = cont.context["image2"] as Image val result = composeImages(image1, image2) load(Continuation(3, cont.context.plus("result" to result), null)) } 3 -> { val result = cont.context["result"] as Image displayImage(result) } } } fun load() = load(Continuation(0, emptyMap(), null)) }
  11. Continuation public interface Continuation<in T> { public val context: CoroutineContext

    public fun resume(value: T) public fun resumeWithException(exception: Throwable) }
  12. Retrofit suspend fun <T: Any> Call<T>.await(): T { return suspendCoroutine

    { cont -> enqueue(object: Callback<T> { override fun onResponse(call: Call<T>?, response: Response<T?>) { if (response.isSuccessful) { val body = response.body() ?: return cont.resumeWithException(NullPointerException()) cont.resume(body) } else { cont.resumeWithException(ApiError()) } } override fun onFailure(call: Call<T>?, t: Throwable) { cont.resumeWithException(t) } }) } }
  13. Retrofit suspend fun <T: Any> Call<T>.await(): T { return suspendCoroutine

    { cont -> enqueue(object: Callback<T> { override fun onResponse(call: Call<T>?, response: Response<T?>) { if (response.isSuccessful) { val body = response.body() ?: return cont.resumeWithException(NullPointerException()) cont.resume(body) } else { cont.resumeWithException(ApiError()) } } override fun onFailure(call: Call<T>?, t: Throwable) { cont.resumeWithException(t) } }) } }
  14. Retrofit interface ApiService { @GET fun loadTweets(): Deferred<List<Tweet>> } launch

    { val tweets = service.loadTweets() displayTweets(tweets.await()) }
  15. Retrofit interface ApiService { @GET fun loadTweets(): Deferred<Response<List<Tweet>>> } launch

    { val deferred = service3.loadTweets() val response = deferred.await() if (response.isSuccessful) { response.body()?.let { displayTweets(it) } } }
  16. Retrofit interface ApiService { @GET fun loadTweets(): Deferred<Response<List<Tweet>>> } launch

    { val deferred = service3.loadTweets() val response = deferred.await() if (response.isSuccessful) { response.body()?.let { displayTweets(it) } } }
  17. async public fun <T> async( context: CoroutineContext = DefaultDispatcher, start:

    CoroutineStart = CoroutineStart.DEFAULT, parent: Job? = null, onCompletion: CompletionHandler? = null, block: suspend CoroutineScope.() -> T ): Deferred<T>
  18. async public fun <T> async( context: CoroutineContext = DefaultDispatcher, start:

    CoroutineStart = CoroutineStart.DEFAULT, parent: Job? = null, onCompletion: CompletionHandler? = null, block: suspend CoroutineScope.() -> T ): Deferred<T>
  19. Coroutine Builders val job = launch { } val deferred

    = async { } val result = runBlocking { }
  20. launch/join val jobs = List(100_000) { launch(context = CommonPool) {

    delay(5000) print(".") } } jobs.forEach { it.join() }
  21. launch/join val jobs = List(100_000) { launch(context = CommonPool) {

    delay(5000) print(".") } } jobs.forEach { it.join() }
  22. launch/join val jobs = List(100_000) { launch(context = CommonPool) {

    delay(5000) print(".") } } jobs.forEach { it.join() }
  23. Coroutine Context val image1 = async { loadImage(url1) } val

    image2 = async { loadImage(url2) } val result = composeImages(image1.await(), image2.await())
  24. Coroutine Context public interface CoroutineContext { public operator fun <E

    : Element> get(key: Key<E>): E? public fun <R> fold(initial: R, operation: (R, Element) -> R): R public operator fun plus(context: CoroutineContext): CoroutineContext public fun minusKey(key: Key<*>): CoroutineContext public interface Element : CoroutineContext public interface Key<E : Element> }
  25. Coroutine Context Thread: ForkJoinPool.commonPool-worker-1
 Context: [DeferredCoroutine{Active}@424f90d, CommonPool]
 Job: DeferredCoroutine{Active}@424f90d
 Interceptor:

    CommonPool Thread: ForkJoinPool.commonPool-worker-2
 Context: [DeferredCoroutine{Active}@4ae30c2, CommonPool]
 Job: DeferredCoroutine{Active}@4ae30c2
 Interceptor: CommonPool
  26. Retry val image1 = retryIO { loadImage(url1) } val image2

    = retryIO(times = 3) { loadImage(url1) } composeImages(image1, image2)
  27. Retry val image1 = retryIO { loadImage(url1) } val image2

    = retryIO(times = 3) { loadImage(url1) } composeImages(image1, image2)
  28. retryIO suspend fun <T> retryIO( times: Int = Int.MAX_VALUE, initialDelay:

    Long = 100, // 0.1 second maxDelay: Long = 1000, // 1 second factor: Double = 2.0, block: suspend () -> T): T { var currentDelay = initialDelay repeat(times - 1) { try { return block() } catch (e: IOException) { Timber.e(e, "retryIO") } delay(currentDelay) currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) } return block() // last attempt }
  29. Lifecycle var job: Job? = null override fun onCreate(savedInstanceState: Bundle?)

    { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) job = launch(UI) { // ... } } override fun onDestroy() { super.onDestroy() job?.cancel() }
  30. Lifecycle var job: Job? = null override fun onCreate(savedInstanceState: Bundle?)

    { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) job = launch(UI) { // ... } } override fun onDestroy() { super.onDestroy() job?.cancel() }
  31. Channels public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> public interface SendChannel<in

    E> { public suspend fun send(element: E) public fun close(cause: Throwable? = null): Boolean } public interface ReceiveChannel<out E> { public suspend fun receive(): E public operator fun iterator(): ChannelIterator<E> }
  32. Channels data class ActivityResult(val requestCode: Int, val resultCode: Int, val

    data: Intent?) private val activityResultChannel = Channel<ActivityResult>() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launch { activityResultChannel.send(ActivityResult(requestCode, resultCode, data)) } } suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult { startActivityForResult(intent, requestCode) return activityResultChannel.receive() } launch { val result = startActivityForResultAsync(intent, 3) }
  33. Channels data class ActivityResult(val requestCode: Int, val resultCode: Int, val

    data: Intent?) private val activityResultChannel = Channel<ActivityResult>() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launch { activityResultChannel.send(ActivityResult(requestCode, resultCode, data)) } } suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult { startActivityForResult(intent, requestCode) return activityResultChannel.receive() } launch { val result = startActivityForResultAsync(intent, 3) }
  34. Channels data class ActivityResult(val requestCode: Int, val resultCode: Int, val

    data: Intent?) private val activityResultChannel = Channel<ActivityResult>() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launch { activityResultChannel.send(ActivityResult(requestCode, resultCode, data)) } } suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult { startActivityForResult(intent, requestCode) return activityResultChannel.receive() } launch { val result = startActivityForResultAsync(intent, 3) }
  35. Channels data class ActivityResult(val requestCode: Int, val resultCode: Int, val

    data: Intent?) private val activityResultChannel = Channel<ActivityResult>() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launch { activityResultChannel.send(ActivityResult(requestCode, resultCode, data)) } } suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult { startActivityForResult(intent, requestCode) return activityResultChannel.receive() } launch { val result = startActivityForResultAsync(intent, 3) }
  36. Channels data class ActivityResult(val requestCode: Int, val resultCode: Int, val

    data: Intent?) private val activityResultChannel = Channel<ActivityResult>() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launch { activityResultChannel.send(ActivityResult(requestCode, resultCode, data)) } } suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult { startActivityForResult(intent, requestCode) return activityResultChannel.receive() } launch { val result = startActivityForResultAsync(intent, 3) }
  37. Channels data class ActivityResult(val requestCode: Int, val resultCode: Int, val

    data: Intent?) private val activityResultChannel = Channel<ActivityResult>() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launch { activityResultChannel.send(ActivityResult(requestCode, resultCode, data)) } } suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult { startActivityForResult(intent, requestCode) return activityResultChannel.receive() } launch { val result = startActivityForResultAsync(intent, 3) }
  38. Channels val channel: Channel<String> = Channel() sendButton.setOnClickListener { launch(UI) {

    val text = usernameEditText.text.toString() channel.send(text) } }
  39. Channels var job: Job? = null override fun onViewCreated(view: View,

    savedInstanceState: Bundle?) { job = launch(UI) { for (string in channel) { resultTextView.text = channel.receive() } } } override fun onDestroyView() { super.onDestroyView() job?.cancel() }
  40. Sequences val fibonacci = buildSequence { yield(1) var cur =

    1 var next = 1 while(true) { yield(next) val tmp = cur + next cur = next next = tmp } } fibonacci.take(5).map { it*it }
  41. Kotlin 1.3 • No longer “experimental” • buildSequence and buildIterator

    moved to the kotlin.sequences package • Simplified Continuation<T>, only one method resumeWith(result:SuccessOrFailure<T>) • KT-16908 Support callable references to suspending functions • KT-18559 Serializability of all coroutine-related classes