Concurrent Algorithms
in Kotlin Coroutines

Half-day tutorial at PPoPP '24

Edinburgh, United Kingdom

March 2, 2024

    Join us at PPoPP '24 to learn and discuss how the core constructs of Kotlin coroutines work under the hood!

    This tutorial is run by those Kotlin team members who have significantly contributed to Kotlin Coroutines. Roman Elizarov introduced coroutines in Kotlin and led the Kotlin team from 2016 until 2023. Nikita Koval recently redesigned the internal synchronization and communication primitives in Kotlin coroutines and is currently working on the Lincheck framework for testing concurrent algorithms while simultaneously leading the Concurrent Computing lab at JetBrains Research.

    Speakers

    Roman Elizarov

    Ex. Kotlin Project Lead

    Nikita Koval

    Head of Concurrent Computing Lab, JetBrains Research

    What is this tutorial about?

    Most mainstream programming languages now offer the async/await mechanism or coroutines abstraction for asynchronous programming. Kotlin stands out with coroutines that occupy a unique spot in the overall language design, using a single suspend modifier at the language level.

    Kotlin coroutines have been widely adopted by the Kotlin user base, representing a recommended solution to concurrency in Android programming and offering concise syntax for reactive server-side applications.

    This tutorial starts with a high-level discussion of what coroutines are and how they work in Kotlin, followed by a series of talks dedicated to particular parts requiring non-trivial concurrent algorithms: a coroutines scheduler, channels, synchronization primitives, and the Lincheck framework designed to test all these algorithms for correctness.

    In recent years, we have also redesigned internal synchronization and communication primitives; much of the work was published at the PPoPP, PLDI, and CAV conferences. We would like to present all the work in one place and discuss the non-trivial trade-offs we made when designing these solutions.

    Program

    March 2, Sat

    • 08:30–09:10 UTC

    Roman Elizarov

    Introduction to Kotlin Coroutines

    What were the design goals of the coroutines in Kotlin, and how are they implemented on the JVM? What is the concept behind suspending functions in Kotlin, and how are they implemented? Exactly what happens when you launch coroutines, and how much actual overhead do they have? This talk answers these and lots of other coroutine-related questions, showing and explaining the corresponding byte code produced by the Kotlin compiler with code samples and benchmarks.

    Introduction to Kotlin Coroutines

    Roman Elizarov

    What were the design goals of the coroutines in Kotlin, and how are they implemented on the JVM? What is the concept behind suspending functions in Kotlin, and how are they implemented? Exactly what happens when you launch coroutines, and how much actual overhead do they have? This talk answers these and lots of other coroutine-related questions, showing and explaining the corresponding byte code produced by the Kotlin compiler with code samples and benchmarks.

    • 09:10–09:50 UTC

    Roman Elizarov

    Coroutines Scheduler

    Coroutines provide end-users with new programming paradigms that present unique challenges to the design of an efficient scheduler, whose task is to allocate coroutines among operating system threads. We will contrast the coroutines workloads to more traditional fork-join workloads, show the benchmarks that drove the design of a dedicated coroutines scheduler, and discuss its algorithm, implementation details, and engineering trade-offs.

    Coroutines Scheduler

    Roman Elizarov

    Coroutines provide end-users with new programming paradigms that present unique challenges to the design of an efficient scheduler, whose task is to allocate coroutines among operating system threads. We will contrast the coroutines workloads to more traditional fork-join workloads, show the benchmarks that drove the design of a dedicated coroutines scheduler, and discuss its algorithm, implementation details, and engineering trade-offs.

    • 09:50–10:30 UTC

    Nikita Koval

    Fast and Scalable Communication Channels

    The key data structure enabling communication between coroutines is the rendezvous channel. In essence, it is a blocking queue of size zero, so both send(e) and receive() operations wait for one another, performing a rendezvous when they meet. To optimize the message-passing pattern, channels are usually equipped with a fixed-size buffer, so sends do not suspend and put elements into the buffer until its capacity is exceeded. This primitive is known as a buffered channel. This talk presents a fast and scalable algorithm for both rendezvous and buffered channels developed for Kotlin coroutines.

    This talk is based on our PPoPP '23 paper.

    Fast and Scalable Communication Channels

    Published at PPoPP '23

    Nikita Koval

    The key data structure enabling communication between coroutines is the rendezvous channel. In essence, it is a blocking queue of size zero, so both send(e) and receive() operations wait for one another, performing a rendezvous when they meet. To optimize the message-passing pattern, channels are usually equipped with a fixed-size buffer, so sends do not suspend and put elements into the buffer until its capacity is exceeded. This primitive is known as a buffered channel. This talk presents a fast and scalable algorithm for both rendezvous and buffered channels developed for Kotlin coroutines.

    This talk is based on our PPoPP '23 paper.

    • 10:30–10:50 UTC

    Coffee Break

    Coffee Break

    • 10:50–11:30 UTC

    Nikita Koval

    CQS: A Formally-Verified Framework for Fair and Abortable Synchronization Primitives

    Programmers often use synchronization abstractions, such as mutex, semaphore, barriers, and count-down latches, to simplify their code. Despite much work in this area, only a few algorithms support both fair FIFO ordering of synchronization requests and abortability. Supporting such semantics is critical for coroutines, which abort frequently and are cheaper to suspend and resume than native threads. This talk introduces a new CancellableQueueSynchronizer (CQS) framework for Kotlin coroutines that enables simple yet efficient implementations of a wide range of fair and abortable synchronization primitives. Besides the algorithmic contribution, we also proved the correctness of all algorithms in Coq and compared them with the primitives in the standard Java library, demonstrating significant improvement.

    This talk is based on our PLDI '23 paper.

    CQS: A Formally-Verified Framework for Fair and Abortable Synchronization Primitives

    Published at PLDI '23

    Nikita Koval

    Programmers often use synchronization abstractions, such as mutex, semaphore, barriers, and count-down latches, to simplify their code. Despite much work in this area, only a few algorithms support both fair FIFO ordering of synchronization requests and abortability. Supporting such semantics is critical for coroutines, which abort frequently and are cheaper to suspend and resume than native threads. This talk introduces a new CancellableQueueSynchronizer (CQS) framework for Kotlin coroutines that enables simple yet efficient implementations of a wide range of fair and abortable synchronization primitives. Besides the algorithmic contribution, we also proved the correctness of all algorithms in Coq and compared them with the primitives in the standard Java library, demonstrating significant improvement.

    This talk is based on our PLDI '23 paper.

    • 11:30–12:30 UTC

    Nikita Koval

    Lincheck: A Practical Framework for Testing Concurrent Data Structures on JVM

    Kotlin coroutines are used to maintain a dozen non-trivial concurrent data structures, which need to be tested properly. Straightforward stress tests are unlikely to catch all possible bugs and flack periodically. The quality of such testing is below the bar. We need a way to write concurrent tests in a convenient, declarative way, keeping in mind "what" we are testing as opposed to "how" we are testing it. With this in mind, the Lincheck framework for writing concurrent tests was introduced. Users list all the data structure operations, and Lincheck verifies whether these operations are thread-safe. This talk presents Lincheck and explains how you can apply it to your setting while also giving a brief overview of the framework’s internals.

    This talk is based on our CAV '23 paper.

    Lincheck: A Practical Framework for Testing Concurrent Data Structures on JVM

    Published at CAV '23

    Nikita Koval

    Kotlin coroutines are used to maintain a dozen non-trivial concurrent data structures, which need to be tested properly. Straightforward stress tests are unlikely to catch all possible bugs and flack periodically. The quality of such testing is below the bar. We need a way to write concurrent tests in a convenient, declarative way, keeping in mind "what" we are testing as opposed to "how" we are testing it. With this in mind, the Lincheck framework for writing concurrent tests was introduced. Users list all the data structure operations, and Lincheck verifies whether these operations are thread-safe. This talk presents Lincheck and explains how you can apply it to your setting while also giving a brief overview of the framework’s internals.

    This talk is based on our CAV '23 paper.

    • 12:30–12:50 UTC

    Open Discussion

    Open Discussion

    How to participate?

    The tutorial is co-located with the PPoPP '24 conference, which takes place on March 2 – March 6 in Edinburgh, UK. You need to register for the conference to participate. Don’t forget to select our tutorial, as it’s not included in the standard ticket!