Skip to content

minced/intro-coroutines-solutions

 
 

Repository files navigation

official JetBrains project GitHub license

Introduction to Coroutines and Channels Hands-On Lab

This repository is the code corresponding to the Introduction to Coroutines and Channels Hands-On Lab.

How this stuff works

Retrofit

The Retrofit library turns API requests into Java commands. In other words:

interface GitHubService {
    @GET("orgs/{org}/repos?per_page=100")
    fun getOrgReposCall(@Path("org")
        org:String):Call<List<Repo>>

    @GET("repos/{owner}/{repo}/contributors?per_page=100")
    fun getRepoContributorsCall(@Path("owner")owner:String, @Path("repo")
        repo:String):Call<List<User>>
}

will allow you to call the API command listed. Details of initializing service are at contributors.GitHubServiceKt.createGitHubService.

That interface is the whole project - sort of. For an organization, get repos. For a repo and its owner, get contributors. Our goal is just to aggregate all the contributors for an organization.

Call

The Java AWT framework has a dedicated event queue.

Other assorted notes

  • also is great for stream processing.
  • -Dkotlinx.coroutines.debug is awesome.

Level 1: Use a background thread

thread {
    loadContributorsBlocking(service, req)
}

Level 2: parallelize tasks

is to parallelize tasks so threads are utilized better. In other words, if task A involves waiting, then do task B in the meantime while you're waiting. Here's the pseudocode:

Step 1: get repos. It's one call. No parallelization here.

After repos fetched, do Step 2:
for each repo: 
  get contributors for this repo.
  and when that's done, enter callback to process users. 

Implementation needs a queue SOMEWHERE. Retrofit has a Call class and Call.enqueue(), which puts a callback in a processing queue.

Advantages of this approach:

  • Callbacks mean that we process users as soon as we can.

Disadvantages:

  • It's REALLY hard to see details like "how many threads are we actually using to process callbacks?"
  • Needs Retrofit API.
  • Hard to write correct code without stuff like countdown latches.
  • Nested callbacks get hard to understand. Our required reading also points this out.

Level 3: suspending functions.

First, the service interface will change to:

interface GitHubService {
    // getOrgReposCall & getRepoContributorsCall declarations

    @GET("orgs/{org}/repos?per_page=100")
    suspend fun getOrgRepos(
        @Path("org") org: String
    ): Response<List<Repo>>

    @GET("repos/{owner}/{repo}/contributors?per_page=100")
    suspend fun getRepoContributors(
        @Path("owner") owner: String,
        @Path("repo") repo: String
    ): Response<List<User>>
}
fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello") // main coroutine continues while a previous one is delayed
}

This will print hello world. In other words, if you start a runBlocking block, other stuff will run when you hit a delay.

A Coroutine Scope limits the lifetime of any coroutines run inside.

What does it buy you? Pros:

  • Coroutines are not lost and do not leak.
  • Any error in the code is reported and never lost.

EXACTLY the same as above:

fun main() = runBlocking { // this: CoroutineScope
    launch { doWorld() }
    println("Hello")
}

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

The coroutine scope builder looks a LOT like runBlocking. It's different. I promise.

For one thing, coroutineScope itself is a suspending function:

runBlocking and coroutineScope builders may look similar because they both wait for their body and all its children to complete. The main difference is that the runBlocking method blocks the current thread for waiting, while coroutineScope just suspends, releasing the underlying thread for other usages. Because of that difference, runBlocking is a regular function and coroutineScope is a suspending function.

In other words, coroutineScope is NOT the entrance to magical suspending land. You need a runBlocking:

fun main() = runBlocking {
    doWorld()
}

suspend fun doWorld() = coroutineScope {  // this: CoroutineScope
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}

About

Solutions to coroutine stuff, along with some toy programs.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Kotlin 100.0%