3

I have a good understanding of when coroutines in Kotlin are superior to threads. I know that coroutines are lightweight and are great for massive concurrency when, in contrast, spinning up lots of threads is costly in compute resources, memory consumption, and possibly other ways.

I also know that coroutines are ultimately backed by threads and that, if you need, you can use a large thread pool so that coroutines can run in parallel, just like threads.

What I am unclear about if there is ever a reason in which you should still go back to threads? Are there cases where threads actually result in better performance, or should I just use coroutines for all my concurrency and parallel processing needs from now on?

4 Answers 4

5

Coroutines try to hide the existence of real threads from your programming model. If required you can pin a coroutine to a thread (by not using suspend functions) so the functionality is largely identical (between manual threads and pinned coroutines).

However the very abstraction that coroutines provide may be a problem, if:

  • You need the identity of the thread.
  • You need fine control of the lifetime of the thread / total number of threads.

The JVM supports the call Thread.interrupt() this will break a thread out of several other operations such as wait(). In order to send the interrupt() from another thread you need to get access to the thread in question. This might be easier to do if you are explicitly starting the threads yourself (i.e you know the identity).

I can't come up with a concrete example of when you would need to explicitly start and stop threads - in that having a pool of threads with some being idle doesn't seem to be particularly problematic, but if such a case exists then again you may need to start threads manually and Thread.join() them at the end.

1
  • Thanks for all the great answers. Everyone has acknowledged that their examples are mostly contrived/theoretical, which makes me think that in general, the answer to my question is "you should probably just use coroutines unless some odd scenario arises". But the examples in this answer feel the least theoretical, so I am accepting this answer.
    – J-bob
    Commented Dec 29, 2023 at 19:48
4

Independent of the language, there may be operating system or external library requirements which mandate that some code runs in separate threads.

For example, a library may use thread local storage so you should call related functions from the same thread. Of course this isn't an argument against using coroutines, but you may need to pin a coroutine to a processor thread if your coroutine system uses multiple threads and would otherwise switch coroutines between threads freely.

On the other hand, there may be operating system calls which are not well suited to being handled with select(), so to achieve non-blocking program behavior you should call these from separate threads.

Both cases are somewhat contrived, I cannot come up with concrete examples on the spot, but maybe this gives some ideas on where to look.

2

I have no real experience with using Kotlin but I'm thinking back to the last time I worked directly with threads in Java. I'm not even sure I actually went with that approach, but I can remember why I considered it.

Essentially the problem I was having was an issue with pushing logging IO to an external aggregator. There would be times when the speed of transmission was slower than the amount being created. That would be OK for a little while but eventually the logging buffer would fill up and this would cause the application to block on writing logs. The goal was to try, as best as possible, to eliminate that from ever happening. So, the theory was that if the listener for the incoming data was being managed at a level above the thread, the IO listener could be paused both because of thread switching and due to the concurrency abstractions. This could result in 'plugging the drain' of the logging framework.

In a nutshell, the idea was to make sure that the logging input was 'sucked in' as soon as possible so that the logger's internal buffer would never be full. Again, I'm not sure I ended up doing this or if I did, whether it was really necessary. The overhead of using a dedicated threads like this is high and it was probably overkill because the internal logging buffer was big enough to tolerate short pauses.

So perhaps, if you have real-time or near-real-time requirement, I could see using a dedicated system thread to minimize delays in reacting to events. I'm not sure this is really going to be effective, though, especially unless you have a fair amount of control over the host environment.

1

Similar to virtual threads, you likely gain nothing from coroutines if all you do is local computation. Instead you have to pay the price to manage them. When using a thread pool you only pay the creation cost of the threads once and for doing computation tasks all you need are as many threads as you have actual cores to fully utilize them.

Note that I'm speaking from a theoretical standpoint, someone with deep Kotlin knowledge might want to correct me, that would be interesting^^

(And btw. if the internals of Kotlin don't work in surprising ways, then whenever you use coroutines, you use at least one thread, you just don't see it).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.