Java virtual threads vs Kotlin coroutines
Asked Answered
B

3

22

How do Java 21 virtual threads will compare to Kotlin coroutines?

When coding in Kotlin, is it better to prefer one over the other?

This video: Java 21 new feature: Virtual Threads #RoadTo21 seems to frown upon using virtual threads for non-IO or non-blocking tasks.

I create coroutines right and left even for CPU-intensive tasks in my Kotlin code. Is this not alright anymore?

Bandeen answered 6/9, 2023 at 16:29 Comment(9)
They're not mutually exclusive. Coroutines can execute on virtual threads or platform threads.Gluttony
@Gluttony Sure, but I think the spirit of the question is - If we made a Venn Diagram about what each one is good for, what would be the XOR value? Or are you implying that because Coroutines can run on VT, that all benefits of VT are a subset of the benefits Coroutine offers?Unknowing
I think there is no clear answer. If you use Kotlin as a syntactic sugar over JVM environment and java libraries, maybe coroutines are not adapted, but if you use multiplatform or Kotlin libraries, then coroutines will be a better fit. You can watch this comparison from Kotlin conf (it might be biased toward coroutines, though)Radiomicrometer
@Radiomicrometer I saw that video too. I hesitated from posting it because I too am concerned about bias. Not to mention, they themselves admitted that they were comparing coroutines against a beta version of VT.Unknowing
@Unknowing Yes, but I've found it informative nonetheless. Another possible comparison : not so long ago, I've read a good article reproducing Go concurrency tour with java Loom. I then tried to reproduce it with Kotlin coroutines instead. I've found it to be a very good experiment, but maybe more thanks to Kotlin language itself than to coroutines. But at least it allow to compare both technologies on the exact same jobs.Radiomicrometer
@Unknowing Yes, that's basically what I'm saying (for the JVM anyway). Coroutines are a concurrency model/paradigm/implementation "above" threads. At least, that's how I understand it. You tell the code which context to run under and call suspending functions. Then the "framework" handles executing the code on the right threads at the right time. So, for example, they could have Dispatchers.IO use virtual threads while Dispatchers.Default uses platform threads.Gluttony
@Radiomicrometer Indeed, very informative. Ultimately, regardless of which tool alone can perform a certain task faster, the fact is, Loom improves much of the ecosystem -- including Coroutines. Whether or not naked Loom solves our problems alone better than Coroutines is irrelevant.Unknowing
Virtual threads and Coroutines are syntactic sugar for event based processing in a shared thread pool (like async/await in JS), so neither is great for CPU intensive tasks that run without pause because you can only share threads in the pool if you pause and wait for e.g. IO. Whether one implementation / style has benefits over the other remains to be seen.Legist
Regarding "virtual threads for non-IO or non-blocking tasks"… Yes, tasks that are entirely CPU-bound are not appropriate for virtual threads which assume switching while blocked. But I suspect truly CPU-bound tasks would be rather rare in most Java apps as that would mean no network calls, no database calls, no calls to storage, and no logging. In those cases of truly being CPU-bound, such as video codec processing or scientific data analysis, use the old-fashioned platform threads in classic Java style. The old style remains intact and unchanged by the arrival of virtual threads.Wood
L
5

Is this not alright anymore? It is alright, Java virtual threads won't replace Kotlin coroutines.

There is a presentation by Roman Elizarov that covers the difference.

Short Recap from presentation

Virtual Threads (Project Loom) are good for

  • Virtual Thread per request
  • Updating existing code

Kotlin Coroutines are good for

  • Highly-concurrent code
  • Event-based systems
  • Structured concurrency and cancellations

And, as mentioned by @Slaw in the comment, coroutines can be executed on virtual threads too. Also coroutines can be used with Kotlin/JS and Kotlin/Native.

Larger answered 6/9, 2023 at 18:51 Comment(0)
Z
1

It is slightly like comparing apples to oranges since the goals are different. Maybe more like comparing peaches to nectarines, because there's a little bit of overlap.

Virtual threads solve the problem of blocking IO tasks taking up an OS thread for their duration. Basically, they reduce the resources necessary to do blocking, synchronous tasks at scale.

Coroutines solve the problems of:

  1. Easily writing concurrent, asynchronous code without bugs (structured concurrency)
  2. Having to nest callbacks to switch back and forth between threads -- code that switches threads can be written synchronously, which makes it much easier to read, write, and reason about. A common case is code that has to transition back and forth between UI work on a main thread that cannot be blocked and IO threads for the blocking work.

Point 1 is eventually coming to Java as well. You can read about it in JEP 453. I haven't studied it in detail, but I kind of assume that in Kotlin, coroutines will still be the preferred way to write concurrent code, because of point 2 and because it can be used in Kotlin multiplatform.

But basically, as it stands, we are comparing things that solve two different problems. But there's no reason you can't use both:

val virtualThreadDispatcher = ExecutorService.newVirtualThreadPerTaskExecutor()
    .asCoroutineDispatcher()

suspend fun foo() = withContext(virtualThreadDispatcher) {
    someBlockingIOCode()
}

Now you have a dispatcher that doesn't have to allocate whole platform threads to run blocking IO code, but at the expense of virtual threads' more expensive task switching. This is kind of a tricky tradeoff to have to think about and balance, but the Kotlin designers' plan is to update Dispatchers.IO to take the best of both worlds once the Java API advances to the point to allow enough customization to achieve it.

Zelig answered 7/9, 2023 at 13:5 Comment(9)
The goals are not different. Point 2 is precisely the goal of virtual threads.Korfonta
@Holger, how would you do this with code that has to work with UI?Zelig
What is “this” in your question?Korfonta
How would you call both blocking work methods and methods that can only touch UI-thread-only classes in sequence without callbacks using virtual threads? That's what point 2 is describing, and my understanding of virtual threads is that they don't provide this capability at all. I understand if you're doing a series of pieces of work that don't care what thread they're done on, virtual threads solve the problem of having to switch threads to avoid occupying threads idly, but this doesn't address the problem of UI threads.Zelig
First of all, I was talking about the goal of writing code which is “much easier to read, write, and reason about” by being able to write it like sequential code instead of chaining or even nesting callbacks. That’s also the goal of virtual threads even if not applicable to the event handling thread which is unfortunately not a virtual thread. But you can still use them to simplify the code in practice. The background thread can be virtual; it has to use a callback for UI interaction but instead of having to chain the next action as another callback, it can wait/block and proceed in sequenceKorfonta
@Korfonta How do virtual threads specifically let us write concurrent code as if it's sequential in a manner similar to that of coroutines? Unless you mean virtual threads are a building block towards that ultimate goal?Gluttony
@Gluttony where did I say that the goal was to do it “in a manner similar to that of coroutines”? Saying that two approaches have the same goal does not imply that one approach’s goal was to be like the other approach. In fact, while having the same goal regarding code which is “easier to read, write, and reason about”, one other goal of virtual threads is to be not like coroutines, i.e. no need for special syntax, no differentiation between “normal code” and “coroutine code” (no limitations on which methods one can call in a virtual thread).Korfonta
@Korfonta But I don't see how virtual threads add anything towards making code "easier to read, write, and reason about". From the perspective of writing code, they are no different than using platform threads. Coroutines change how code is written in an attempt to make concurrent code more understandable. I would agree that JEP 453 (structured concurrency) has the same, or at least similar, goal as Kotlin coroutines, just at an API level rather than a language level (i.e., a different approach). But virtual threads? I thought the goal was to make blocking operations more scalable.Gluttony
@Gluttony conventional concurrent code tries to avoid blocking calls, leading to frameworks like CompletableFuture, Flow, etc, where you have to chain callbacks which may chain more callbacks, etc, which is hard to read and horribly to debug. With virtual threads, you don’t have to avoid blocking calls, hence, can write an ordinary sequence of statements with ordinary control flow, using blocking waits when necessary. An example has been given already, the I/O background thread can simply wait for the UI thread’s response and proceed, instead of switch back and forth with callbacks.Korfonta
L
1

From this video, we can say that Loom is great. But it's not a replacement for kotlin coroutines. Coroutines are still the recommended thing to use when dealing with concurrent processes in Kotlin. To compare them,

  • Loom can improve the performance applications: it can run multiple virtual threads and it costs less to have blocked virtual threads than to have a regular threads blocked. -Kotlin coroutines are intrusive because we cannot call suspend functions in normal function, which is not the case of Loom
  • Structured concurrency is much more easier with Kotlin coroutines than Loom.
  • Interoperability between Kotlin coroutines and reactive programming are more simpler as we can just use flows than the one between loom and reactive programming.

To wrap up, we can say that the best thing Loom has to propose is the virtual threads which can inprove performance, while Kotlin coroutines has more to offer. Why not using both of the features together?

Well, there is a concept introduced by Moskala in his "Kotlin Coroutines: Deep dive" book which is interesting. We can use Loom directly in Kotlin coroutines code and have a better performance while keeping structured concurrency and all the cool stuff we get in coroutines. To do that, we use virtual threads to replace Dispatchers.IO. Below is an example of code that uses virtual thread.

val LoomDispatcher = Executors
  .newVirtualThreadPerTaskExecutor()
  .asCoroutineDispatcher()
val Dispatchers.Loom: CoroutineDispatcher
  get() = LoomDispatcher

suspend fun main() = measureTimeMillis {
  coroutineScope {
    repeat(100_000) {
      launch(Dispatchers.Loom) {
        Thread.sleep(1000)
      }
    }
  }
}.let(::println)

Because Dispatchers.IO has just 64 threads, the code above will take over 26 minutes, but we can make use of limitedParallelism to increase the number of thread and get the chance to execute the code fastly. The code using Dispatchers.IO is as follow:

suspend fun main() = measureTimeMillis {
  val dispatcher = Dispatchers.IO
    .limitedParallelism(100_000)
  coroutineScope {
    repeat(100_000) {
      launch(dispatcher) {
        Thread.sleep(1000)
      }
    }
  }
}.let(::println)

I didn't personnally run the code with virtual threads but it's said in the book that it took a bit more than two seconds to execute (This is amzing knowing that we block each of the 100 000 threads for 1 second), but the second code took around 30s to finish executing.

I would say the best way to make the code performant and use some cool stuff that coroutines offer, we can use Loom as a substutition for Dispatchers.IO but keep using coroutines, though there is no any kind of recommendations in the documentations.

Linage answered 29/11, 2023 at 10:12 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.