Coroutines for decoupled event handling in Kotlin/Native

Coroutines are a nice way for managing asynchronous processing, except on Kotlin/Native it is not possible to use coroutines on multiple threads at the time of the creation of this article. Nevertheless, it can be quite useful even on the main thread only. This article describes one usecase.

Event loops in lcarswm

The central part of the lifecycle of many programs is the event loop. Here a program waits for requests from the user or other programs. Sometimes the event loop is written explicitly in the software as shown in the following figure, sometimes it can be handled by a framework.

int main(int argc, const char* argv[]) {
    // startup the program

    while (true) {
        // event loop
        // poll for events and handle them
        
        // cleanup for shutdown
    }
}
Abstract lifecycle of a C program

This can get a bit weird when it is required to handle events from different event sources. Which event source should be handled first in the run of the loop? What is the optimal frequency to request new events from the event sources? These and other questions came to me when I had the same situation in lcarswm. The two event sources that are here so far are the X display management and a tool program that communicates with the window manager using a message queue. Both event sources need to be polled for new events and lcarswm doesn't use a framework that brings its own event loop management. Needless to say, I didn't like the first solution and I will not show that here. Feel free to check the commit history of lcarswm to find the past implementation, if you are interested.

Implementation using coroutines

Coroutines work only on the main thread of a program in Kotlin/Native (Kotlin 1.4 just came out at the time of writing this article). But that is enough for implementing an event loop mechanism for independent multiple event sources. The key idea is to launch one coroutine per event loop. The coroutines can handle the event sources independently from each other. Additionally, the fact that the coroutines run on the same thread - the main thread - ensures that there are no problems from asynchronous accesses of any resources (as long as there is no broken state after handling an event). The coroutines can even have different delays in them. That way one coroutine can be prioritized by having a shorter delay as sleep time before asking for another event. By the way, delays don't block a thread. In general, an event loop with one coroutine could look like this:

fun main() = runBlocking {
    // startup the program

    val eventLoopJob = launch {
        while (true) {
            // event loop
            // poll for events and handle them
        }
    }

    eventLoopJob.join()

    // cleanup for shutdown
}
Event loop in coroutine in Kotlin

How about two coroutines? Here is where it gets interesting. How do the event loops stop? In the case of lcarswm only the event loop that handles X display manager events completes by itself. The event loop for communication with the tool app would run forever and therefore the program would never get past its join call. What we can do is cancel a coroutine from the outside. It is also possible to add callables to the completion of a coroutine. For lcarswm this means that a callable can be attached to the completion of the X event loop coroutine and that callable cancels the tool communication coroutine. Because the X event loop coroutine will always be shut down first, the program can wait (join) solely for the other coroutine to make sure both coroutines are closed before running the shutdown sequence. The following figure shows an abstraction of the code of lcarswm.

fun main() = runBlocking {
    // startup the program

    val toolEventLoopJob = launch {
        while (true) {
            ...
        }
    }
    val xEventLoopJob = launch {
        while (true) {
            ...
        }
    }

    xEventLoopJob.invokeOnCompletion {
        toolEventLoopJob.cancel()
    }

    toolEventLoopJob.join()

    // cleanup for shutdown
}
Running to independent event loops using coroutines

Now the event loops are decoupled and can be handled independently. Maybe in the future there will be additional event loops. In that case, new coroutines can be created for each of them. The important thing is to also cancel them on completion of the main (X event) coroutine and call join on them to make sure they are completed as well.

If you are curious how the real code for the event loops looks like check out runEventLoops.kt from lcarswm on Github.