Hacker News new | ask | show | jobs
by eggsnbacon1 2233 days ago
the author leaves out Kotlin which adds support for coroutines on the language level and still compiles to java bytecode. These are not classic continuations because they cannot be cancelled, but they're still very useful and true fibers.

There's also the Quasar library that adds fiber support to existing Java projects, but its mostly unmaintained since the maintainers were pulled in to work on Project Loom.

Then there's Project Loom, an active branch of of OpenJDK with language support for continuations and a fiber threading model. The prototype is done and they're in the optimization phase. I expect fibers to land in the Java spec somewhere around JDK 17.

I figure its fair to mention these as the authors criticisms are somewhat valid but will not be for very long (few years max?)

In summary: Java will have true fiber support "soon". This will invalidate the arguments for Erlang concurrency model. They are already outdated if you are okay using mixed java/kotlin coroutines or Quasar library

The newer Java GC's Shenandoah and ZGC address authors criticisms of pause times. They already exist, are free, and are in stable releases. Dare I say they are almost certainly better than Erlang's GC. They are truly state of the art, arguably far superior to the GC's used in Go, .NET, etc. Pause times are ~10 milliseconds at 99.5%ile latency for multi terabyte heaps, with average pause times well below 1 millisecond. No other GC'ed language comes close to my knowledge. His points 1 and 2 no longer exist with these collectors. You don't need 2X memory for the copy phase and the collectors quickly return unused memory to the OS. This has been the case for several years.

Hot code reloading. JVM supports this extensively and its used all the time. Look into ByteBuddy, CGLIB, ASM, Spring AOP if you want to know more. Java also supports code generation at build time using Annotation Processors. This is also extensively used/abused to get rid of language cruft

7 comments

> This will invalidate the arguments for Erlang concurrency model.

What about failure domains? As far as I'm concerned, this is the strongest reason for actor-based concurrency. I can design my architecture so that groups of processes that need to die together die together. And it's usually one or two lines of code, if any.

Here's a real life example. I have a process that maintains an SSH connection to a host machine, and that ssh connection is used to query information about running VMs on that host machine. If the SSH connection dies, it kills the process that is tracking the host machine, which in turn kills the processes tracking the associated VMs, without perturbing any of the other hosts' processes or vms. This triggers the host process to be restarted by a supervisor, which then creates a new SSH connection to query for information (possibly repopulating VM processes for tracking information). All of this I wrote zero lines of code for (which, importantly, means I made no mistakes), just one or two configuration options. More importantly, the system doesn't get stuck in an undefined state where complex query failures can cause logjams in the running system.

You can tie the fates of threads together in Java using thread groups. If you need more flexibility, or want it to be managed for you, Akka framework offers this. I believe Akka gives you a model very similar to Erlang.

In Java you would create a thread pool and configure it to restart the threads if they die. Each thread would wake up every so often to query SSH and dump their results into a queue. If the query threads die, the processes reading the queue at the other end have nothing to do so they won't execute. Its easy to make a consumer queue that executes some code on another thread whenever data arrives.

Java's exposure of the underlying OS threads and cheap transfer of data between threads lets people build libraries on top that offer memory models used by Erlang and others. Its not built in or quite as convenient, but you can use actors and fibers in Java if you want to.

Yeah that's exactly the problem. It's an afterthought in the system. How certain can you be that the system you're using is compostable with any other code brought in to your system, even from libraries outside? In erlang, failure domains are the raison d'etre of the language, so everything in the ecosystem will play nice.

Ultimately, systems like akka are extremely complicated to get right, even for experts, because you have to think about all of the vm bits underneath. I can (and have) teach a junior programmer basic OTP concepts with the confidence that they can't mess things up. Now, they wouldn't be able to come up with the architecture I designed as a good idea, but I could tell them to implement it (with tests!) and expect them to get it right.

That's what exceptions are for, no? If a connection dies an exception is thrown that would propagate up to the top of the thread stack. You'd then catch it and sit in a loop re-establishing the SSH connection, or terminating with a signal to whatever thread started the monitoring thread that it was dying. The act of unwinding the stack would pass through the finally handlers, closing open resources and cleaning up, before the loop starts again.

The failure domain here isn't precisely defined because shared data is allowed (but not required). You could define it as "anything reachable from the thread/fiber stack".

No. If you try to use exceptions to guard your failure domains in this fashion you will not have a good time.
A current discussion on the Loom mailing list is about providing Structured Concurrency [1] primitives.

It would allow you to write something like:

    try (var scope = FiberScope.open(Option.PROPAGATE_CANCEL)) {
        var fiber1 = scope.schedule(() -> sshKeepAlive());
        var fiber2 = scope.schedule(() -> trackHost());
        var fiber3 = scope.schedule(() -> trackVMs());
    }
With the garantee that if any fiber fails (which you bind to cancelling it), all others will be cancelled.

[1] http://250bpm.com/blog:71

Java's GC has to be best in class because of shared memory. In a shared nothing world doing GC in one "thread" doesn't stop other threads from executing, it also means that each heap can be very small so you might not even need to perform gc before the thread is done executing. It's truly amazing what Java is doing, but keep in mind that Erlang has worked this way for _decades_. And still, a classic web server that spins up one thread/process per request, can still potentially end up responding to the request with zero garbage collection in the best case, irrespective of load. This will not be true for Shenendoa or ZGC.

Does Java's Hot code reloading support data migration? One benefit of Erlangs model is that you can execute hooks when HCR is performed to make sure your data in memory is migrated to a new format.

But really, the most important thing about Erlangs actor model is error handling. If I spin up a process in Erlang and it fails, it won't corrupt the state of my other processes. In Java this can only be attained through disipline since all memory is shared. Also, I can very easily specify which processes should work together as units, such that if one fails, they all fail, and can be restarted together from a known working state. This, again, requires discipline in Java.

Per thread GC is definitely a different approach than Java takes. The trade-off is that shared memory between Java threads is nearly free. Basically the same approach C++ uses, except Java has better concurrency primitives because its VM. Not sure about Erlang but data sharing between processes on JS and Python is very expensive and a frequent criticism of those languages. You can achieve zero garbage per request in Java. Typically high performance web frameworks like Undertow and Vert.X are designed this way. User code rarely does it but its definitely possible.

Not sure what you mean by data migration on code reloading. I suspect the mechanisms are different enough that it can't be compared. With Java you can load arbitrary new code, but changes to existing code are limited in ways that prevent data incompatibilities. For example you can add fields to existing object but you can't change the type of existing ones.

Data corruption from threading is rare in Java. I can't remember the last time I ran into it. Its easy to do but everyone is used to threads and the concurrency implementation is one of the best I've used. Java also supports thread groups to ensure that threads die and get restarted together. Its not automatic, you need to manage the groups, but I think it achieves the same.

In Erlang processes need to send messages to each other. And those messages are copies (nothing is shared). This is less efficient than in Java where everything is shared, but it also means that process a cannot change something that process b is looking at. So locks in Erlang, aren't necessary. It also enables easy distribution. When all processes share data by messaging, it doesn't matter if those processes are running on the same machine or are distributed on a network.

Since Erlang has one GC per process, you can create garbage in one process without triggering GC if that process is short lived. Once the process dies, the entire heap for that process is returned to memory. So in Java, you'd have to write code in a special way to avoid GC, but in Erlang that happens automatically if either your process exits before the heap for that process needs GC. And in Erlang it's pretty normal to run one process per http request, so this does happen in practice, without requiring anything of the programmer.

When it comes to hot code reloading and data migration. When you hot load code into an Erlang vm, a hook will be called if defined which allows you to migrate all data that is in memory into new format. So, you're not restricted by data-incompatibility.

Your last paragraph is what I referred to by required discipline. Everyone that touches the code is required to understand what causes corruption and what doesn't. It also requires that you know which classes are thread safe and which arent, which is hopefully documented somewhere. Thread groups need to be understood (I work in Java/Kotlin every day, and I didn't know what thread groups were before today). In Erlang, data corruption due to multiple processes doesn't happen, and grouping processes together (supervision trees) is so common I can't remember the last time I saw an Erlang program without one.

Which of course doesn't mean that Erlang is superior to Java. But when you're working on something highly concurrent which needs to be fault tolerant, I'd argue that you'd get a better result with less effort than in Java. But of course, if you know Java really well and don't know Erlang at all, YMMW.

Different strokes I guess.

Erlang's model with fibers and message passing sounds close to Golang. Java has decent support for immutable objects with immutable collections, Lombok, the FreeBuilder library, both build-time code generators, and Java 14 record types. Automatic passing between machines is unique to Erlang

Per process GC isn't anything like Java does, but the new GC's are probably fast enough that it doesn't matter in practice. For any sane sized heaps the GC pauses are around 0.5 millisecond. This wasn't true until a few years ago, and in production most people don't know or care enough to use the new GC's.

You are right about thread safety in objects. Thankfully the JDK surface is fully documented. Third party libraries usually are. Internal code is a crapshoot. It requires discipline, but I still find it rare in practice because the normal patterns lend themselves to thread safety.

I think its safe to say that Java is a lower level language than Erlang which enables many of the same patterns with less convenience. You can probably get better performance with Java, but your fault tolerance completely depends on how good your coders are. Java will not save you from doing stupid things between threads.

Sounds about right :)

Just wanted to touch on one point. Golang also has shared memory, even though it encourages sharing by communicating. In Erlang you don't have a choice. Golang also doesn't have something like supervision trees (threads that die and restart together). So in practice golang and erlang concurrency is very different.

Interesting, so Golang channels are basically a hybrid between the two approaches.

I envy Rust and its borrow checker. Its a pain to get used to, but enables "shared when you say it is" concurrency model with no overhead. No message passing overhead, optional but safe mutability, no data corruption possibility, zero copy basically everywhere

Fair point - all this is true.

As a counter-point, I've been working on a platform for the last few years which uses Kotlin and Quasar in production. Quasar was cool at first but now it's just a nightmare and I wish we never opted to use it. It leaks abstractions all over the place with @Suspendable annotations and users of the platform find the quasar related errors super confusing. Debugging is also very difficult because of Quasar. On the other hand, Kotlin is great!

If I could turn back the time, I'd build the messaging/async workflow part of the platform using Erlang. I've mentioned this to a few people but they all think I'm mad... "Erlang... are you on drugs?!", which is disappointing because it's literally perfect for our use case.

I have actually heard the same about Quasar so I have avoided using it. It hacks up the bytecode so evil bugs appear common based on my glances at issue tracker.

Why didn't you use Kotlin coroutines? My understanding is that they achieve the same as Quasar without the insanity.

You may also want to look at Vert.X. Its evolved into a lot more than a REST framework. It uses thread-per-core and nonblocking to achieve high performance instead of green threads. It theoretically performs better because there's not a lot of stacks hanging around and only 1 thread per core. There's a lot of callbacks though, so if you're not used to RxJava style chaining its hard to get used to. Its very much like Node.

Erlang or Go would be the easiest if you need a lot of threads. If you just need high performance with a lot of connections, Vert.X may suffice. Java IO in recent years is fully non-blocking so you don't need a lot of threads for high concurrency. Vert.X can handle millions of concurrent clients, enough that you will need to adjust your kernel to hit its limits. And its built on Netty which is rock solid.

Sounds like you made the right decision! When we started the project four years ago, coroutines were still quite experimental so wasn't a feasible option for us. If I were to consider all the trade-offs, moving to coroutines not something we should do now as I believe it would yield only marginal benefits but would break compatibility for customers.

One of the main problems with the Quasar/coroutine based model is that the semantics are quite hard to undersand for developers who are not very familiar with concurrency. They write code that _looks_ synchronous but is actually async. We get a lot of support tickets claiming there's a bug in the platform when the reality is that they don't understand what's going on. I sympathise with them and we probably need to do a better job of hiding the complexities. As you note, the bytecode instrumentation is a bit of a pain but not only that... It also has quite a big impact on performance!

There has been talk of doing some experiments with Akka and that's something I'm interested in exploring. But I think, hypothetically, that writing parts of the platform again in Erlang/OTP would yield huge productivity benefits... gen_fsm offers exactly what we need out of the box. From the little playing around I've done with Erlang, it feels like you can get a small, competent team, up to speed fairly quickly.

You may also look at Kotlin coroutines in VertX, that we are using and seem to work just fine.
I have been looking at this for an upcoming project where we need to handle a ton of persistent HTTP clients. Regular Vert.X is "fine" but TBH having all the callbacks sucks. My only reservation to using Kotlin is IDE support. I know its great in IntelliJ but licenses are expensive and I don't want to advocate something that ties us to a single IDE. Lots of our guys use Eclipse and VSCode.

I know there's plugins for Kotlin support in other IDE's, have you used them and if so are they any good?

IDEA Community Edition has full Kotlin support and is free.
Non-preemptive concurrency doesn't invalidate any argument. Erlang's GC is per user thread, even a primitive GC per user thread will have lower latency than Java's GC.
I want to see a source on this. Golang's GC is often touted as better than Java's but every real world benchmark I've seen shows that it sacrifices a lot of throughput for low pause times, essentially by running much more often.

Java's new garbage collectors, ZGC and Shenandoah, have average pause times of 0.3 milliseconds on heaps less than 4GB. I find it unlikely that another language has pause times shorter than that given the sheer amount of work put into Java GC over the years

> have average pause times of 0.3 milliseconds on heaps less than 4GB

The answer is that one user thread would have a lot less memory than 4GB, and the GC only needs to work on heap sizes of KBs to MBs for most cases.

There is no shared memory(well, mostly).

It's comparing apples and oranges.

Don't forget ZIO (and other Cats Effect based libraries) which is the new kid on the block, and has it's own take on fibers and concurrent programming.

The biggest problem with Erlang is that hardly anything out there needs this level of concurrency and robustness in a single system - in the new world of microservices and serverless architectures there are other ways to cope with scaling. This is the main selling point, and unfortunately in all other areas Erlang is significantly outdated and refuses to evolve - even less so than the Java language which is a dinosaur in itself.

Having said that I think Erlang is a fantastic teaching tool and should be on everyone's bucket list of "things to learn in this life as a software engineer".

> The biggest problem with Erlang is that hardly anything out there needs this level of concurrency and robustness in a single system - in the new world of microservices and serverless architectures there are other ways to cope with scaling.

I wondered about this myself before I started using Elixir. In practice, it turns out when it's cheap to make things concurrent more services take advantage of this feature.

Tests and the elixir compiler are extremely fast because of this, and it makes the whole development experience better.

Because the primitives are so simple, people experiment more which makes better software. Nobody would come up with phoenix live view for Play framework in their spare time because play framework is so overly complicated.

Yes, that's a problem, you can't be the only one responsible for a core part of the stack.

I did an interview at a major streaming company and one critical part was written in Erlang. It has been working great but the guy who wrote it had left for some time and nobody knew Erlang there, so they would have to rewrite it if an update was needed.

Or, someone could take a week to learn Erlang and make whatever change is needed. You won't be an Erlang expert in a week, but it's a pretty small language, so editing existing code isn't that hard. And that existing code can't be that bad, since it's still working.
These are not classic continuations because they cannot be cancelled, but they're still very useful and true fibers. what? Coroutines do support cancelation
Is Java the best out there?