Room

Suspending Functions

Many operations in a multi-threaded application end up blocking the current thread, and it cannot be used for any other work while it's blocked. Think about a sleep function. You've got some background work running, and it wants to pause for a moment (perhaps you're checking if something has changed once a minute). This ties up the thread that's being used to run that operation.

Threads are a limited resource. Using them like this severely limits concurrency on your machine. What we'd like to do is allow other operations to be performed while we're waiting.

This is where suspending functions come into play. A suspending function is one that can be paused when it's called, with the current state of its caller captured.

Coroutines are cooperative. Java threads are pre-emptive.

Long ago, multitasking was all about being cooperative. Your program would start running on a CPU, and would keep running until you would yield, telling the CPU it was ok to give another program a turn. Your state was saved, the CPU would run something else, and it would yield to allow you (or something else picked by the scheduler) to run.

But all it took was one rogue program to hog the CPU and starve all other programs until it was done. This wouldn't necessarily be done by a malicious programmer; it was easy to accidentally omit the yield, as it depended upon the programmer reading the right chunk of documentation.

Pre-emptive multitasking got rid of that worry. When multiple programs wanted to run at the same time, the CPU scheduler would grant each a chunk of time on one of its threads, then switch to another program. It wasn't up to the programmer.

Kotlin is most often run on top of the Java Virtual Machine (JVM), which uses a pre-emptive threading model. When we use cooperative coroutines, we get an interesting mix of threading models that works well in our favor. A coroutine cannot starve other processes that want to use a thread, as the pre-emptive nature of the JVM will force switches.

But if a blocking operation (like a sleep) is performed, the CPU will keep switching back to the operation, wasting time that other processes could use.

Coroutine work is scheduled in a way that can decide when it really needs to use a thread. Instead of blocking a thread from being used while sleep is called, we use delay, which interacts with the scheduler to request to be continued at a later time. Other coroutines can then use the underlying thread, allowing much better concurrency.

Marking a function with the suspend keyword causes the Kotlin compiler to modify that function to pass an additional parameter called a "continuation". Whenever a suspend function is called from within it, the continuation remembers our function state so we can restart at that point later. The scheduler can then use the thread that the suspend function was being run on. Later, when our suspend function gets another chance to run, the continuation is used to execute the next chunk (up until another suspend function is called). For more detail on how this works, check out Suspend functions - Kotlin vocabulary.

Suspending functions can only be called from another suspend function or within a coroutine, started by launch (no result needed) or async (allows us to wait for a result).

Dispatchers manage one or more threads as a group. As we've seen earlier, we use dispatchers to switch the coroutine processing to work on a different thread using the withContext(dispatcher) function, which is itself a suspending function!

Google's current advice is that suspending functions should be "main-thread safe". If the function is called from the main (UI) thread, it should use withContext to switch to a different dispatcher.

For example, if we wanted to define a function to update a person in the database, we could write

suspend fun updatePerson(person: Person) {
    withContext(Dispatchers.IO) {
        database.dao.update(person)
    }
}

This function is main-safe, as it forces a switch to the IO dispatcher. Often you'll see suspend functions written using Kotlin's single-expression-function syntax. (withContext will return the value that its lambda returns).

suspend fun updatePerson(person: Person) = withContext(Dispatchers.IO) {
    database.dao.update(person)
}

Because this is a common task to perform, and because Room can perform some extra optimization if it knows it's running in a coroutine, we can add suspend to our DAO function declarations. The Room compiler will create a main-safe suspending function for you:

@Dao
interface PersonDao {
    ...
    @Insert
    suspend fun insert(vararg person: Person)

    @Update
    suspend fun update(vararg person: Person)

    @Delete
    suspend fun delete(vararg person: Person)

    @Query("DELETE FROM Person WHERE id IN (:ids)")
    suspend fun delete(ids: List<String>)
}

This forces you to launch coroutines to call these functions (suspend functions can only be called from coroutines or other suspend functions) and switches to a dispatcher that Room defines to optimize the database access.