Search code examples
javakotlinconcurrencyoperating-system

Is there any disparity in fundamental concepts among modern concurrency models


I've recently been studying various modern concurrency models, such as Virtual Thread (JDK 21), Kotlin coroutines, the Event Loop within Spring Webflux (which may refer to Netty), and related topics like Java NIO or multiplexing with 'epoll'. However, as I delved into these, I've noticed that they seem quite similar from the following perspectives (please excuse any mixed-up keywords or terminology, as I'm still a novice):

  1. They all share the same goal of achieving high concurrency with a limited number of threads.
  2. Elements like Virtual Threads, Tasks, and Events are used to process code but may potentially become blocked.
  3. If blocking occurs during execution, the executor (or thread) defers its current task and proceeds with another task.
  4. The blocked task can resume execution after an I/O operation.

So, here are my questions:

  1. Is there anything incorrect in the list above? Please point out any misunderstandings if they exist.
  2. If everything in the list is accurate, is the only difference between these models related to how they are used? (e.g., their APIs, concepts, or mental models)

I realize that these questions may sound ambiguous, basic and also dumb, but I'd greatly appreciate any insights you can provide. Thank you in advance.


Solution

  • Is there anything incorrect in the list above? Please point out any misunderstandings if they exist.

    Yes, I would agree with your list (except maybe 2. where I am not entirely sure whether you refer to the operation needing to be put on hold or whether you are talking about actually blocking the (platform) thread when talking about "blocking").

    The idea behind these constructs is that threads are a (somewhat) valuable resource. While you want to handle a lot of requests (or other operations) concurrently, you can't have as many threads as requests/tasks/operations you want to perform. So the idea behind these concepts is to get a few threads doing work and if some request/task/operation requires performing some I/O or blocking operation, some other request/task/operation can use the thread in the meantime. This way, you can increase CPU utilization i.e. you are not wasting time where the CPU is idle.

    Elements like Virtual Threads, Tasks, and Events are used to process code but may potentially become blocked.

    What these tools do is allow you to reuse threads while some operation would need to wait for something (e.g. I/O). As a programmer, you should make sure that you are not actually doing something blocking the (platform) thread.

    The difference

    If everything in the list is accurate, is the only difference between these models related to how they are used? (e.g., their APIs, concepts, or mental models)

    What is different between these approaches is their implementation and how they can be used.

    virtual threads

    Virtual Threads are implemented in the JVM. You can just write normal code and the virtual threads will automatically yield/unmount the carrier thread and let other threads continue when performing a blocking operation. In contrast to other methods, this allows you to actually use blocking operations like Socket#accept while maintaining the said concurrency benefits (though it should be noted that there are still some cases where you might not get the performance you want, e.g. in case of pinning for too long or performing operations like file I/O where it might need to extend the thread pool if it can't be done asynchronously (internally).

    With virtual threads, you can use stack traces and all the tooling like you would with normal blocking code without using virtual threads.

    To summarize, virtual threads take existing blocking operations and let other threads do work in the meantime.

    (Java) reactive frameworks

    Reactive frameworks in Java are not written inside the JVM, they are written on top of it. If you are using reactive frameworks, you shouldn't use any blocking operations (if possible). Instead, you should use some operation that is compatible with the framework and register a callback. The framework then manages what is executed when.

    Kotlin coroutines

    Kotlin coroutines internally work like reactive frameworks (or it will use a reactive framework internally) but they have language support.

    So, Kotlin coroutines are also built on top of the JVM and not part of the JVM so you shouldn't call any actual blocking operations on them. However, you can certainly call suspend functions and wait for them to finish and Kotlin coroutines (together with the reactive framework) will take of running your code at the right time without you needing to register callbacks by yourself. For example, you should use delay() (which can handle that) instead of Thread.sleep().

    Essentially, Kotlin coroutines provide an abstraction over reactive frameworks that is part of the language. You should still not call bllcking operations but you don't have to deal with having all of these callbacks/lambdas.
    However, that does limit you to using Kotlin.