loading...

re: Why I stopped using Coroutines in Kotlin VIEW POST

TOP OF THREAD FULL DISCUSSION
re: Can you provide an example for libraries that expect a thread-based environment without coroutines? Besides, in my experience with Android, the cum...
 

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.

 

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.

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.

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.

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.

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.

Code of Conduct Report abuse