Asynchronous Programming and Coroutines

Synchronous (Blocking) execution

Synchronous (Blocking) execution, 2 Threads

Asynchronous (Non-Blocking) execution

Asynchronous (Non-Blocking) execution (2 Threads)

Asynchronous Execution

  • Execution of a computation on another computing unit.
  • Without waiting for its termination
  • Better resource efficiency.

Concurrency Control of Asynchronous Programs

What if a function depends on the result of another function ?

fun coffeeBreak() {
    val coffee = makeCoffee()
    drink(coffee)
    chatWithColleagues()
}

fun makeCoffee(): Coffee {
    println("Making Coffee")
    // Work for some time
    return Coffee()
}

fun drink(coffee: Coffee) { }

Asynchronous Implementation using Callbacks

fun coffeeBreak() {
    makeCoffee { coffee ->
        drink(coffee)
    }
    chatWithColleagues()
}

fun makeCoffee(coffeeDone: (Coffee) -> Unit) {
    println("Making Coffee")
    // Work for some time
    val coffee = Coffee()
    coffeeDone(coffee)
}

fun drink(coffee: Coffee) { }

From Synchronous to Asynchronous Type Signatures

A synchronous type signature can be turned into an asynchronous type signature by:

  • Not returning a value
  • Taking as parameter a continuation defining what to do after
    the return value has been computed.
fun program(a: A): B { 
    // Do Something
    return B()
}
fun program(a: A, c: (B) -> Unit) {
    // Do Something
    c(B())
}

The callback Hell

What if another program depends on the coffee break to be done?
For example conference includes a cofeeBreak in the middle:

fun coffeeBreak(breakDone: () -> Unit) {}
fun conference() {
    presentation { p1 ->
        coffeeBreak {
            presentation { p2 ->
                endConference(p1, p2)
            }
        }
    }
}

Also called The pyramid of doom

Summary

What we have seen so far:

  • How to sequence asynchronous computations using callbacks
  • Callbacks introduce complex type signatures
  • The continuation passing style can lead to a pyramid of doom

From Synchronous to Asynchronous Type Signatures

(using Futures)

Remember the transformation we applied to a synchronous type signature to make it asynchronous:
fun program(a: A): B {}
fun program(a: A, k: (B) -> Unit) {}

What if we could model an asynchronous result of type T as a return type Future<T>?


fun program(a: A): Future<B> {}

Currying

Essentially with are moving:

fun program(a: A, k: (B) -> Unit) : Unit {}

To

fun program(a: A): ((B) -> Unit) -> Unit {}

Remember Haskell ?

add :: Num a => a -> a -> a
add x y = x + y

-- The type of `add 10` will be
add 10 :: Num a => a -> a

Coffee Break using Future

fun makeCoffeeFuture(): Future<String> {
    return CompletableFuture.supplyAsync {
        println("Making Coffee")
        sleep(2000)
        "coffee ${coffeeNumber.incrementAndGet()}"
    }
}

fun futureCoffeeBreak() {
    val f: Future<String> = makeCoffeeFuture()
    chatWithColleagues()
    drink(f.get())
}

Not blocking on get

import java.util.concurrent.CompletableFuture

fun futureCoffeeBreak() {
    val f: CompletableFuture<String> = makeCoffeeFuture()
    f.thenAccept { coffee ->
        drink(coffee)
    }
    chatWithColleagues()
}

Handling Errors

import java.util.concurrent.CompletableFuture

fun futureCoffeeBreak() {
    val f: CompletableFuture<String> = makeCoffeeFuture()
    f.thenAccept { coffee ->
        drink(coffee)
    }
    .handle { r, exception ->
        println("Failed with $exception")
    } 
    chatWithColleagues()
}

Combining Futures

fun futureCoffeeBreakBlended() {
    val f1 = makeCoffeeFuture()
    val f2 = makeCoffeeFuture()
    val combinedFuture = f1.thenCombine(f2) { result1, result2 ->
        "$result1 blended with $result2"
    }
    combinedFuture.thenAccept { c ->
        drink(c)
    }
    chatWithColleagues()
}

Futures are Monads

Again Remember haskell

blendedCoffee = 
    do 
        coffee1 <- makeCoffeeFuture 
        coffee2 <- makeCoffeeFuture
        return coffee1 ++ " Blended With " ++ coffee2 

In Javascript they are called Promises

let coffeeNumber = 0;

function makeCoffeePromise(): Promise<string> {
    return new Promise((resolve) => {
        console.log("Making Coffee");
        setTimeout(() => {
            coffeeNumber += 1;
            resolve(`coffee ${coffeeNumber}`);
        }, 2000);
    });
}

function coffeeBreak() {
    const f: Promise<string> = makeCoffeePromise();
    f.then(coffee => {
        drink(coffee);
    })
    chatWithColleagues();
}

Await/Async syntax sugar

async function coffeeBreak(): Promise<void> {
    
    const f: Promise<string> = makeCoffeePromise();
    chatWithColleagues();
    
    const coffee = await f;  
    // The code below will be executed when the promised is fullfilled
    drink(coffee);
    
    // Promised is propagated! 
}

JavaScript is Single-Threaded

  • Origins in Browsers: javascript was designed to manipulate the DOM in web browsers.
  • Single-threaded model prevents conflicts and inconsistencies.
  • Event Loop: javascript operates on an event-driven model. Event loop checks for tasks like user inputs, network requests, and timers.
  • Ensures responsiveness by processing one event at a time.

The event loop

Coroutines

Basic Concepts

  • Subroutines vs Coroutines
    • Subroutines: Single entry point, single exit
    • Coroutines: Multiple entry points, can pause and resume
  • Yielding Execution
    • Coroutines can yield control back to the caller
    • Resume from the point they were paused

Coroutines vs Subroutines

Coroutines

Coroutines First Example (Kotlin)


suspend fun makeCoffee(): String {
    println("Making Coffee")
    delay(2000)  // Using delay from kotlinx.coroutines instead of Thread.sleep
    return "coffee ${coffeeNumber.incrementAndGet()}"
}

fun coffeeBreakWithCoroutines() = runBlocking {
    launch {
        val coffee = makeCoffee()
        drink(coffee)

    }
    chatWithColleagues()
}

Benefits of Coroutines

  • Simpler Code

    • Write asynchronous code that looks synchronous
    • Easier to read and maintain
  • Concurrency

    • Manage multiple tasks concurrently
    • Avoid the complexity of threads
    • Very lightweight

Lightweight concurrency

fun main() = runBlocking {
    for (i in 0..50_000) { // launch a lot of coroutines
        launch {
            println("Hello $i!")
            delay(5000L)
        }
    }
}

Sequences (Lazy collections)

val fibonacciSeq: Sequence<Int> = sequence {
    yield(1)
    yield(1)
    var a = 1; var b = 1

    while (true) {
        val result = a + b
        yield(result)
        a = b; b = result
    }
}

fun main() = fibonacciSeq.take(10).forEach { print("$it ") }

Sequential vs Parallel execution

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) ; return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L); return 29
}
fun main() = runBlocking {
    val time = measureTimeMillis {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
}

Parallel execution

fun main() = runBlocking {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

Threads, Fibers and Coroutines

  • Thread
    • State: User-mode stack + kernel mode stack + context
    • Run by an OS scheduler
    • Unit of suspension: entire thread, CPU is free to run something else
  • Fiber (aka User-Mode-Thread, Virtual Thread)
    • State: User-mode stack + Context
    • Run by some thread
    • Unit of suspension: fiber, underlying thread is free to run
  • Coroutine
    • State: Local variables + Context
    • Run by some thread or fiber
    • Unit of suspension: coroutine, underlying thread/fiber is free to run