Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Asynchronous Programming With Kotlin Coroutines

Asynchronous Programming With Kotlin Coroutines

Loveleen Kaur

May 08, 2023
Tweet

More Decks by Loveleen Kaur

Other Decks in Technology

Transcript

  1. • Loveleen Kaur • Software Engineer - Android @ Astrotalk

    • Core Team Member @ GDG Chandigarh • Android Educator @ Android Educators Community India • Phd Research Scholar • Develop Mobile Applications • Technical Speaker • Happy Android Developer :) Who am I? 󰠁
  2. A toy problem Kotlin fun requestToken(): Token { // makes

    request for a token & waits return token // returns result when received } 1
  3. fun requestToken(): Token { … } fun createPost(token: Token, item:

    Item): Post { // sends item to the server & waits return post // returns resulting post } A toy problem 2 Kotlin
  4. A toy problem Kotlin fun requestToken(): Token { … }

    fun createPost(token: Token, item: Item): Post { … } 3 fun processPost(post: Post) { // does some local processing of result }
  5. fun requestToken(): Token { … } fun createPost(token: Token, item:

    Item): Post { … } fun processPost(post: Post) { … } A toy problem fun postItem(item: Item) { val token = requestToken() val post = createPost(token, item) processPost(post) } 1 2 3 Can be done with threads! Kotlin
  6. fun requestToken(): Token { // makes request for a token

    // blocks the thread waiting for result return token // returns result when received } fun createPost(token: Token, item: Item): Post { … } fun processPost(post: Post) { … } fun postItem(item: Item) { val token = requestToken() val post = createPost(token, item) processPost(post) } Threads Is anything wrong with it?
  7. Callbacks: before fun requestToken(): Token { // makes request for

    a token & waits return token // returns result when received } 1 Callbacks: after 1 callback fun requestTokenAsync(cb: (Token) -> Unit) { // makes request for a token, invokes callback when done // returns immediately }
  8. Callbacks: before fun requestTokenAsync(cb: (Token) -> Unit) { … }

    fun createPost(token: Token, item: Item): Post { // sends item to the server & waits return post // returns resulting post } 2 Callbacks: after fun requestTokenAsync(cb: (Token) -> Unit) { … } fun createPostAsync(token: Token, item: Item, cb: (Post) -> Unit) { // sends item to the server, invokes callback when done // returns immediately } 2 callback
  9. Callbacks: before fun fun requestTokenAsync(cb: (Token) createPostAsync(token: Token, -> Unit)

    { … } item: Item, cb: (Post) -> Unit) { … } fun processPost(post: Post) { … } fun postItem(item: Item) { val token = requestToken() val post = createPost(token, item) processPost(post) }
  10. Callbacks: after fun fun requestTokenAsync(cb: (Token) createPostAsync(token: Token, -> Unit)

    { … } item: Item, cb: (Post) -> Unit) { … } fun processPost(post: Post) { … } fun postItem(item: Item) { requestTokenAsync { token -> createPostAsync(token, item) { post -> processPost(post) } } } aka “callback hell” This is simplified. Handling exceptions makes it a real mess
  11. • The Kotlin team defines coroutines as “lightweight threads”. •

    They are sort of tasks that the actual threads can execute. • Coroutines were added to Kotlin in version 1.3 and are based on established concepts from other languages. • Kotlin coroutines introduce a new style of concurrency that can be used on Android to simplify async code. Coroutines
  12. Coroutines: before fun requestTokenAsync(): Promise<Token> { // makes request for

    a token // returns promise for a future result immediately } 1 Coroutines: after suspend fun requestToken(): Token { // makes request for a token & suspends return token // returns result when received } 1
  13. Coroutines: before suspend fun requestToken(): Token { … } fun

    createPostAsync(token: Token, item: Item): Promise<Post> { // sends item to the server // returns promise for a future result immediately } 2 Coroutines: after suspend fun requestToken(): Token { … } suspend fun createPost(token: Token, item: Item): Post { // sends item to the server & suspends return post // returns result when received } 2
  14. Coroutines: before fun postItem(item: Item) { requestTokenAsync() .thenCompose { token

    -> createPostAsync(token, item) } .thenAccept { post -> processPost(post) } } Coroutines: after suspend fun postItem(item: Item) { val token = requestToken() val post = createPost(token, item) processPost(post) } suspension points
  15. • Regular exception handing try { createPost(token, item) } catch

    (e: BadTokenException) { … } Bonus features
  16. Bonus features • Regular higher-order functions file.readLines().forEach { line ->

    createPost(token, line.toItem()) } • forEach, let, apply, repeat, filter, map, use, etc Everything like in blocking code
  17. Retrofit async interface Service { fun createPost(token: Token, item: Item):

    Call<Post> } suspend fun createPost(token: Token, item: Item): Post = serviceInstance.createPost(token, item).await()
  18. Retrofit async suspend fun createPost(token: Token, item: Item): Post =

    serviceInstance.createPost(token, item).await() interface Service { fun createPost(token: Token, item: Item): Call<Post> } natural signature
  19. Retrofit async interface Service { fun createPost(token: Token, item: Item):

    Call<Post> } suspend fun createPost(token: Token, item: Item): Post = serviceInstance.createPost(token, item).await() Suspending extension function from integration library
  20. Coroutines revisited suspend fun requestToken(): Token { … } suspend

    fun createPost(token: Token, item: Item): Post { … } fun processPost(post: Post) { … } suspend fun postItem(item: Item) { val token = requestToken() val post = createPost(token, item) processPost(post) }
  21. Coroutines revisited suspend fun requestToken(): Token { … } suspend

    fun createPost(token: Token, item: Item): Post { … } fun processPost(post: Post) { … } fun postItem(item: Item) { val token = requestToken() val post = createPost(token, item) processPost(post) }
  22. Coroutines revisited suspend fun requestToken(): Token { … } suspend

    fun createPost(token: Token, item: Item): Post { … } fun processPost(post: Post) { … } fun postItem(item: Item) { val token = requestToken() val post = createPost(token, item) processPost(post) } Error: Suspend function 'requestToken' should be called only from a coroutine or another suspend function
  23. Coroutines revisited suspend fun requestToken(): Token { … } suspend

    fun createPost(token: Token, item: Item): Post { … } fun processPost(post: Post) { … } fun postItem(item: Item) { val token = requestToken() val post = createPost(token, item) processPost(post) } Can suspend execution
  24. Coroutines revisited suspend fun requestToken(): Token { … } suspend

    fun createPost(token: Token, item: Item): Post { … } fun processPost(post: Post) { … } fun postItem(item: Item) { val token = requestToken() val post = createPost(token, item) processPost(post) } Can suspend execution A regular function cannot
  25. Coroutines revisited suspend fun requestToken(): Token { … } suspend

    fun createPost(token: Token, item: Item): Post { … } fun processPost(post: Post) { … } Can suspend execution A regular function cannot fun postItem(item: Item) { val token = requestToken() val post = createPost(token, item) processPost(post) } One cannot simply invoke a suspending function
  26. } Launch coroutine builder fun postItem(item: Item) { launch {

    val token = requestToken() val post = createPost(token, item) processPost(post) }
  27. fun postItem(item: Item) { launch { val token = requestToken()

    val post = createPost(token, item) processPost(post) } Fire and forget! Returns immediately, coroutine works in background thread pool }
  28. fun postItem(item: Item) { launch { val token = requestToken()

    val post = createPost(token, item) processPost(post) } }
  29. fun postItem(item: Item) { launch(UI) { val token = requestToken()

    val post = createPost(token, item) processPost(post) } } UI Context Just specify the context
  30. fun postItem(item: Item) { launch(UI) { val token = requestToken()

    val post = createPost(token, item) processPost(post) } } UI Context And it gets executed on UI thread
  31. Kotlin-way Kotlin suspend fun postItem(item: Item) { val token =

    requestToken() val post = createPost(token, item) processPost(post) } suspend fun requestToken(): Token { … } suspend fun createPost(token: Token, item: Item): Post { … } fun processPost(post: Post) { … }
  32. Classic-way C# approach to the same problem (also Python, TS,

    Dart, coming to JS) C# async Task postItem(Item item) { var token = await requestToken(); var post = await createPost(token, item); processPost(post); } async Task<Token> requestToken() { … } async Task<Post> createPost(Token token, Item item) { … } void processPost(Post post) { … }
  33. Why no await keyword in Kotlin? The problem with async

    requestToken() VALID –> produces Task<Token> await requestToken() VALID –> produces Token concurrent behavior sequential behavior C# C# default
  34. Kotlin suspending functions are designed to imitate sequential behavior by

    default Concurrency is hard Concurrency has to be explicit
  35. Kotlin async function fun loadImageAsync(name: String): Deferred<Image> = async {

    … } val deferred1 = loadImageAsync(name1) val deferred2 = loadImageAsync(name2) Start multiple operations concurrently Kotlin
  36. Kotlin async function fun loadImageAsync(name: String): Deferred<Image> = async {

    … } val deferred1 = loadImageAsync(name1) val deferred2 = loadImageAsync(name2) and then wait for them val image1 = deferred1.await() val image2 = deferred2.await() await function Suspends until deferred is complete Kotlin
  37. Kotlin async function fun loadImageAsync(name: String): Deferred<Image> = async {

    … } val deferred1 = loadImageAsync(name1) val deferred2 = loadImageAsync(name2) val image1 = deferred1.await() val image2 = deferred2.await() val result = combineImages(image1, image2) Kotlin
  38. Using async function when needed suspend fun loadImage(name: String): Image

    { … } Is defined as suspending function, not async
  39. Using async function when needed suspend fun loadImage(name: String): Image

    { … } suspend fun loadAndCombine(name1: String, name2: String): Image { val deferred1 = async { loadImage(name1) } val deferred2 = async { loadImage(name2) } return combineImages(deferred1.await(), deferred2.await()) }
  40. Using async function when needed suspend fun loadImage(name: String): Image

    { … } suspend fun loadAndCombine(name1: String, name2: String): Image { async val deferred1 = { loadImage(name1) } val deferred2 = async { loadImage(name2) } return combineImages(deferred1.await(), deferred2.await()) }
  41. Using async function when needed suspend fun loadImage(name: String): Image

    { … } suspend fun loadAndCombine(name1: String, name2: String): Image { val deferred1 = async { loadImage(name1) } async val deferred2 = { loadImage(name2) } return combineImages(deferred1.await(), deferred2.await()) }
  42. Using async function when needed suspend fun loadImage(name: String): Image

    { … } suspend fun loadAndCombine(name1: String, name2: String): Image { val deferred1 = async { loadImage(name1) } val deferred2 = async { loadImage(name2) } return combineImages(deferred1.await(), deferred2.await()) }
  43. Kotlin approach to async requestToken() VALID –> produces Token async

    { requestToken() } VALID –> produces Deferred<Token> sequential behavior concurrent behavior Kotlin Kotlin default
  44. fun main(args: Array<String>) = runBlocking<Unit> { val jobs = List(100_000)

    { launch { delay(1000L) print(".") } } jobs.forEach { it.join() } } Example
  45. fun main(args: Array<String>) = runBlocking<Unit> { val jobs = List(100_000)

    { launch { delay(1000L) print(".") } } jobs.forEach { it.join() } } Example This coroutine builder runs coroutine in the context of invoker thread
  46. fun main(args: Array<String>) = runBlocking<Unit> { val jobs = List(100_000)

    { launch { delay(1000L) print(".") } } jobs.forEach { it.join() } } Example
  47. fun main(args: Array<String>) = runBlocking<Unit> { val jobs = List(100_000)

    { launch { delay(1000L) print(".") } } jobs.forEach { it.join() } } Example
  48. fun main(args: Array<String>) = runBlocking<Unit> { val jobs = List(100_000)

    { launch { delay(1000L) print(".") } } jobs.forEach { it.join() } } Example Suspends for 1 second
  49. fun main(args: Array<String>) = runBlocking<Unit> { val jobs = List(100_000)

    { launch { delay(1000L) print(".") } } jobs.forEach { it.join() } } Example We can join a job just like a thread
  50. Example Try that with 100k threads! fun main(args: Array<String>) =

    runBlocking<Unit> { val jobs = List(100_000) { launch { delay(1000L) print(".") } } jobs.forEach { it.join() } } Prints 100k dots after one second delay
  51. fun main(args: Array<String>) = val jobs = List(100_000) { runBlocking<Unit>

    { launch { delay(1000L) print(".") } } jobs.forEach { it.join() } } Example
  52. fun main(args: Array<String>) { val jobs = List(100_000) { thread

    { Thread.sleep(1000L) print(".") } } jobs.forEach { it.join() } } Example
  53. Kotlin coroutines Standard library kotlinx-coroutines launch, async, runBlocking, future, delay,

    Job, Deferred, etc http://github.com/kotlin/kotlinx.coroutines