DEV Community

Why I stopped using Coroutines in Kotlin

Martin Häusler on June 22, 2019

It's not a secret that I'm pretty enthusiastic about Kotlin as a programming language, despite a few shortcomings and strange design choices. I got...
Collapse
 
lukasz_bialy profile image
Łukasz Biały

Your arguments apply to basically every async framework / toolkit there is on JVM including event loop based ones, actor based ones or effect monad based ones. This applies to Spring WebFlux as well, so basically the only thing you have ascertained is that asynchronous programming on the JVM requires some trade-offs and/or progress in tooling space. Final argument about "just spawning new instances of your thread-per-request Spring based macroservices" is just plain misinformation and praise of laziness - you suggest spawning countless instances of JVM (of all runtimes probably the most resource-hungry) using thread blocking just because you feel mildly inconvenienced by a different programming paradigm when the same can be achieved with significantly smaller amount of running instances.

Source: I'm a big data / reactive systems engineer working in Scala.

Collapse
 
martinhaeusler profile image
Martin Häusler

That's not a real argument. If it were, all of us would still code in C, or even assembler, because it is "more efficient". We're not talking about "minor inconveniences" here; each of the problems listed in the blog post are potential dealbreakers in their own right.

What kind of information do you want to convey with your "sorce"? Is this the modern version of "I'm a pro so you better believe it"? lol

Collapse
 
lukasz_bialy profile image
Łukasz Biały

Are you seriously comparing using C vs a jvm language with using sync I/O vs async I/O? Because for IO bound apps a java non-blocking app can easily be more efficient than a blocking app in whichever compiled to native language. Your arguments are minor inconveniences - it's not a deal breaking problem that you can't use step over in debugger. It's not a deal breaking problem that your threads are shifted under your code. It's just inconvenient to your habits and it's not only possible but relatively simple to learn to debug async code on the jvm with current tooling.

Source clause was meant to convey information that I'm not pulling my opinion out of a hat. Asynchronicity, thread shifting and non blocking I/O are something completely normal in Scala ecosystem.

Collapse
 
skittishsloth profile image
Matthew Cory

Having to add a new breakpoint is a potential deal breaker? Sorry, in my debugging sessions I've got anywhere from 10-20 going at a time and I add and remove them as the need arises.

Having to pass state along as a parameter to functions (unless you can get the coroutine context going) is a practice I'd encourage anyways over thread local. I've had to rewrite stuff at work because it depended on ThreadLocal instead of a supplied state, and reactive code has the same "issues" as coroutines. We had issues of our own figuring out how to get a reactive context working with Flux/Mono code so we wrote our own stuff to deal with it. Took a day, give or take, and we can use that elsewhere.

The only point you've made that's a potential deal breaker is regarding locks and synchronized blocks. That's not something I considered and thank you for pointing it out. Roman commented on this thread; I'd love to hear his take on that because I've never read anything about whether they're needed or how they're supposed to be dealt with (if at all). Maybe you came across a legitimate bug.

That said, I've been fortunate enough that in almost 20 years of Java work I've yet to work with threads to such a level I really needed synchronization or locks. I've found it's far easier to create immutable objects in spite of the added boilerplate they require; then you don't have to worry about race conditions. I have yet to come across an instance where that's not possible, but I'm sure they're out there and when that comes up this would be good to know.

Lukasz' argument applies perfectly. It has nothing to do with efficincy. You're having problems dealing with a different way of handling something you're used to doing. Instead of approaching this as "here's some gotchas I found in coroutines and how to work around them" you came it as they're crap and you recommend avoiding them. And because you legitimately think an extra breakpoint is a problem I'm not inclined to take your opinion very highly on this.

You should respond to Roman. He's got a bit of insight on Kotlin and coroutines. His feedback might be useful.

Collapse
 
psfeng profile image
Pin-Sho Feng • Edited

+1 to the counter-arguments in this thread.

I think Kotlin's coroutines are an impressive piece of engineering but if there's something I regret it's that it encourages to write code in an imperative style. Note that that's not a problem with coroutines per-se, they're low-level powerful constructs that can be used to build higher-level libraries.

With imperative style it's only a matter of time plus sufficient amount of different hands and a few deadlines that the code becomes spaghetti. Then you add some state to it and it becomes a non-parallelizable mess.

I'd encourage you to try to avoid sharing state and actually pass the arguments you need. Even though it might seem annoying initially, it's going to help make your functions pure, with all the advantages of that (parallelization, for example). Also, there are patterns to mitigate the problem (e.g. the Reader monad in the functional world). Studying functional programming would help you see things in a different way.

Regarding locks, the documentation is clear that you should use Mutex, if you really have to. You could also try to use thread-safe data structures. Still, I believe that you could probably find a way to avoid sharing variables. Maybe look into actors?

I would have to agree with you on the debugger's issue though, but at least there's a workaround.

Collapse
 
elizarov profile image
Roman Elizarov • Edited

Thanks for sharing your experience. You write:

Whenever you call a coroutine builder, such as async, runBlocking or launch, you will - by default - lose your current coroutine context

Can you please elaborate what does that mean? Can you give some kind of self-contained example that demonstrates this problem?

Collapse
 
martinhaeusler profile image
Martin Häusler

I've updated the blog post. Unfortunately this topic is quite complex and would require an article in its own right. I've added a link to an external blog post which discusses exactly this issue in detail.

Collapse
 
elizarov profile image
Roman Elizarov

Now it makes sense. The blogpost you've linked to describes a pre-release version of kotlinx.coroutines library which indeed used to have this problem. Before making a stable 1.0 release we had introduced the concept of “Structured Concurrency” makes inheritance of the coroutine context a default behavior and solves a host of other problems. That is why I could not understand how you could be still having this problem. What version of kotlinx.coroutines library were you using?

Btw, you can read more about structured concurrency here: medium.com/@elizarov/structured-co...

Collapse
 
fkirc profile image
Felix K • Edited

Can you provide an example for libraries that expect a thread-based environment without coroutines? Besides, in my experience with Android, the cumbersome debugger variables are more problematic than stepping over a suspend function.

Collapse
 
martinhaeusler profile image
Martin Häusler

For instance, I'm using guava caches a lot. Basically you provide a loading function which is executed on cache miss and will produce the missing value. The cache implementation takes care if eviction policies etc... It's really a powerful library. However, the function you pass in is called under a lock, and guava protects the programmer from recursive calls to loading the same key. Furthermore, concurrent requests to the same missing key always result in just a single call to the loader function. All of that is based on threads. Having the loader function use coroutines simply won't work.

Collapse
 
fkirc profile image
Felix K

I agree that this is something that should be fixed by the guava developers. Nevertheless, I still recommend that JVM developers should start using coroutines as soon as possible.

Thread Thread
 
martinhaeusler profile image
Martin Häusler

How would the guava developers ever go about "fixing" this? They can neither assume nor refute the presence of coroutines. Coroutines will split the JVM ecosystem in half - and I know on which side I'm on. I do have some hope for Project Fiber, a JVM extension which will bring JVM built-in coroutines. The big advantage here is that:

  • Project Fiber will take into account things like monitors, locks and ThreadLocals.
  • The project will extend the Java compiler to actually tell you where using locks is potentially invalid because you may reach this point from a coroutine.

The big folly is to assume that in the presence of coroutines we do not need synchronization primitives any longer. It's quite the contrary, we need them more than ever before, except we disarmed ourselves by forfeiting the tools which have been working and well understood for years. As long as thread code and coroutine/fiber code don't play nice with each other - through JVM extensions, compiler verification or black magic - I'm not going to touch coroutines anymore. I've had my fair share of bad experiences.

Thread Thread
 
fkirc profile image
Felix K • Edited

kotlinx.coroutines.sync.mutex is a mutex implementation that works with coroutines. If guava was using this mutex implementation, then it might work. Alternatively, guava could release all its internal locks when executing your loading function. If your loading function needs special synchronization, then you can still implement that without relying on the guava locking. Besides, recursively loading the same key seems like a strange edge case. One should not query guava from within the loading function that is supposed to retrieve the very same key. The protection from this error is nice to have but not a major dealbreaker.

Thread Thread
 
martinhaeusler profile image
Martin Häusler • Edited

Nevertheless, recursively loading the same key seems like a strange edge case. One should not query guava from within the loading function that is supposed to retrieve the very same key.

Indeed it is, and not what the developer wants. Which is why guava raises an exception if this case occurs. But take a step back and think about how guava accomplishes that. It checks internally if the current thread holds the loading lock, and if so, it checks the key it wants to load. If it is the same as during the previous call, the exception is thrown. Now, let's assume the loader is async. What would that mean? Well, the coroutine may yield during loading (e.g. when performing an HTTP request to fetch the data) and the host thread will merrily move along and pick up the next coroutine. It still holds the lock, however. This has two fatal consequences:

  • when our loader finishes, there is no guarantee that it will be exe cuted by the same thread which holds the appropriate lock.
  • the thread which holds the lock may host a coroutine which may query the same missing key. Guava will throw an exception here, thinking that it is dealing with recursive load calls to the same key, when in reality that is not the case.

Using coroutines in an environment which isn't specifically crafted for them (read: 99.9% of all JVM libraries in existence) means opening pandoras box. This is precisely what the fancy presentations will not tell you. And the reason why I refactored a lot of code to eliminate coroutines entirely. I've never looked back.

Thread Thread
 
fkirc profile image
Felix K • Edited

Either you revert it, or you adapt your code to cope with new paradigms. As outlined, this should be easy to fix for the guava developers. In the meantime, a possible workaround is to wrap your loading functions inside a runBlocking scope. According to the docs:

runBlocking is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in main functions and in tests.

In fact, I believe that Kotlin enables a more gradual transition to coroutines than building coroutines into the JVM. Adding a few Kotlin coroutines might be easier than switching a myriad of libraries to a new JVM version.

Collapse
 
benjishults profile image
benjishults

These are all important issues to be aware of. I come to a different conclusion. Here's what has happened in other languages with coroutines: tools, frameworks, and best practices are developed and these problems either go away or become managed very naturally. I suspect that once that happens, we will all be using coroutines so much that junior developers will be using them without knowing that these issues ever existed.
In the meantime, we have to be careful about all these important issues you brought up... and we sometimes have to place another break-point. :D

Collapse
 
rjaros profile image
Robert Jaros

You can find several counterarguments in Kotlin Slack channel, where I've posted a link to your article:
kotlinlang.slack.com/archives/C1CF...

Collapse
 
vach_himself profile image
Vachagan Balayan

Martin you should give it another try, use it with the best practices. I think guys at JB rushed a bit making all of these features available to people (though that why its called experimental).

Either way i would totally agree with you a while back, but now they figured out how to use it properly, things you can do with co routines i don't think there is any other tech out there that allows this... its far more capable than plain async or event loops.

For example i use it to make the code very straight forward for things like complex order executions, where i interact with various exchanges send orders cancel them await for cancellations, combine these simple executions to create more complex ones... or cancel them and see how other smaller orders get cancelled properly, it is very ugly to do it in blocking or future world... but its a poetry if you use coroutines.

Another thing i use it for is telegram bots... each users conversation is one very synchronous looking piece of code, which is very simple to understand and change. For me there is no performance chase its just code readability.

Collapse
 
lulzmachine profile image
lulzmachine

How often do you normally "suspend" in a coroutine? Are you required to suspend every time you perform some IO task? (In order to "give up" the thread to someone else).

I thought naïvely that you would just start your coroutine at the start of each new HTTP request entering the server, and then keep the coroutine until the request is done. But I guess that will block the thread, just like in JS or so?

I was hoping coroutines could be an escape from Futures/Promises. They have similar problems: for every time you call a function, you have to know whether it (or a child of it) is asynchronous. This makes, for instance, HTML template rendering in JS almost impossible.

Collapse
 
evgenyvaganov profile image
evgenyvaganov • Edited

I don't fully understand the issue with threads. I see that the problem might exist when e.g. you call coroutine1->coroutine2 and you have synchronized between them coroutine1->synchronized->coroutine2 in that case it is true the call to coroutine2 might end up on a different thread and the lock won't be unlocked. That issue might exist when one coroutine calls another or coroutine calls another async api which might be adjusted to it (e.g. CompletableFuture, Reactor, RxJava, etc).

But I don't understand your example with Guava cache. You won't be able to pass suspend function to guava hence the thread which returns to Guava will be the same. In order to call coroutines from non-coroutines you will need to have boundary call (runBlocking, GlobalScope.async, etc) which will protect you from thread flip. I cannot even image situation where I could get this issue (except when you call synchronized from coroutines (which you should probably never do)).

The other issues - yes debugging is quite annoying thing which you could overcome by having multiple breakpoints (I would describe it as medium level inconvenience). Also it is not really issue of the coroutines as concept or implementation, it is a one in Kotlin Idea Plugin debugging capabilities (which from my layman perspective shouldn't be very difficult to fix - just to have a hidden breakpoint at the next line). Passing down context isn't an issue anymore. We pass tenant related info using coroutine context.

Collapse
 
patroza profile image
Patrick Roza

Sounds like a pain, wonder why it is so much better in JavaScript, but especially C#; co-routines are the bread and butter there.