Why Continuations Are Coming to Java
infoq.com
infoq.com
> However, actually, Project Loom, the goal of the project is to add continuations, fibers, and tail call elimination.
I'm guessing that the project is called either Loom or Loon (and i believe it's the former), but i like the idea that there are actually two cooperating projects, each of which occasionally suspends and lets the other run.
Another way of putting it is that yes, Operating Systems do do it. Thats what an OS-level thread is.
Context switching is relatively expensive. The CPU needs to push a lot of application state out of the way to clear the path for the OS code to run, then after the OS is done, push the OS out of the way and retrieve the application's state and code again.
Whereas a fiber remains entirely inside the application code. It never requires a context switch. But a fiber loses some of the powers of the OS: for example, it can't draw hard memory boundaries between fibers that will be enforced by the CPU.
For some things you want processes, for some threads, for some fibers.
It’s the OS context switch that makes process threads so much more costly than user-scheduled fibers. You cannot involve the OS if you want efficiency.
Otherwise it is the same exact thing.
Like with most things, there are drawbacks as well as benefits.
In this age of VMs/containers for everything I'm not convinced a conventional OS offers a lot of value - OSes made sense when programs needed to access different kinds of hardware and we liked to have multiple processes/users sharing a single machine while broadly trusting each other, but neither of those things is really true any more. Look at unikernels for where I think the future is going - bootable VMs that act as a language runtime that controls things like threading directly, no need for an OS intermediary.
This reads to me like you believe the OS must memcpy/move the whole stack out of the way on a context switch. It doesn't; the other thread has other, dedicated memory its stack, and on a context switch, the stack pointer is simply adjusted to point at the other stack.
Assuming Java's fibers work similar to other green thread implementations, green threads/fibers work similarly — it's just a pointer change, except the adjustment is done in userspace.
(And, to some degree, the OS does know what bytes are stack for any given task. It's whole pages, yes, but that allows the program to manage it otherwise. But I think most green-thread/userspace threads are similar: the stack is preallocated ahead of time and left to the thread to manage. Sure, you might not know the exact range, but you don't really need to? Go, I think, is an interesting outlier here; IIRC, it dynamically expands and contracts the allocated space on the stack in response to the application's demands, though I do think they had some interesting issues w/ loops thrashing allocations if they fell along an allocation boundary. I think they've also long since fixed that issue.)
> The current prototype implements the mount/dismount operations by copying stack frames from the continuation stack – stored on the Java heap as two Java arrays, an Object array for the references on the stack and a primitive array for primitive values and metadata. Copying a frame from the thread stack (which we also call the vertical stack, or the v-stack) to the continuation stack (also, the horizontal stack, or the h-stack) is called freezing it, while copying a frame from the h-stack to the v-stack is called thawing. The prototype also optionally thaws just a small portion of the h-stack when mounting using an approach called lazy copy; see the JVMLS 2018 talk as well as the section on performance for more detail.
Source: https://wiki.openjdk.java.net/display/loom/Main
The video presentation describes it better. I think in their expected usage scenarios the stacks of fibers aren't particularly deep so the copying isn't that expensive. Also, IIRC, doing it this way was less intrusive to the existing JVM architecture; it's possible in time they'll rearchitect things to use a more traditional technique.
https://blog.tsunanet.net/2010/11/how-long-does-it-take-to-m...
But allocating the address space even if it is not committed still consumes finite resources and still limits the number of threads you can create.
Handling memory allocation lazily like this is necessary to handle a number of edge cases, such as spinning up a massive number of short-lived threads. It also prevents thrashing of the TLB cache.
In practice, real operating systems finely tune their behavior here. I would not be surprised at all if a 4kB allocation is made for a thread's stack upon creation in modern operating systems. But I would be very surprised if, e.g., Linux allocated a full 1MB of memory at thread creation time instead of handling the vast majority of it lazily.
EDIT: Oh wait, I think you were mostly agreeing with me :) Yes, my original comment did mess up address allocation vs physical page allocation due to a brain fart. I meant it the other way around and I think we're saying nearly the same thing.
The one major point of difference is that to make an address allocation in the page table doesn't require a physical allocation. The OS can either leave that allocated space unconfigured, or assign it a protected page table. In either case it faults on access and the OS knows that before killing the process with core exception to first look in its internal lazy delayed-allocation tables to see if it the access was to an allocated area of the address space with deferred allocation.
BTW, I studied a little deeper, Java actually commits the stack memory upon thread creation.
The memory distribution of java process may be seen using jcmd <pid> VM.native_memory as suggested here http://xmlandmore.blogspot.com/2014/09/jdk-8-thread-stack-si...
But due to Linux memory overcommit this memory is not really allocated.
I've managed to create 127000 Java threads on my machine, and the resident memory of this process, as shown by `top` is 2,167G ~ 17k per thread.
If I disable memory overcommit, only around 32000 threads are created.
No it does require a physical allocation. The page table entry for the new virtual memory needs to be a physical allocation!
People say 'you can have 256 TB of virtual memory'! Yes you can, but you will need 256 GB of physical memory to hold the page table for that, won't you, assuming 4 KB pages, even if none of that 256 TB is committed to physical memory.
You say 'that's not how the OS's page management tables work' - yes it is! Look up Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3, Chapter 4, formats of page-directory entry and page-table entry. It's committed memory.
Application threads are a part of the application runtime environment. Thread scheduling is little more than a function call and doesn't involve a syscall invocation, and can be hooked into runtime environment trigger mechanisms.
Kernels have to be one size fits all, application threads can be tuned more precisely, with greater visibility over the application's state.
In the case of managing the stack, the kernel must be able to support languages like C/C++ and Rust, that can have internal pointers pointing into the stack itself. This makes it hard to reallocate stacks dynamically and move them around in memory. The JVM doesn't allow such pointers in application code, and internal bookkeeping pointers are known.
As to the scheduler, the kernel must support very different kinds of threads: threads that serve server transactions that block very often, and threads that, say, encode a video, that rarely block at all. These different kinds of threads are best served by different schedulers (e.g. the "frequently blocking" kind of thread is best served by a work-stealing scheduler, but that may not be the best scheduler for other kinds of threads). When you implement fibers in the runtime, you can let the developer choose a scheduler for their needs.
Also, is there any timeline or estimate when Loom will be released with official JDK?
From another place in this thread:
As OpenJDK has switched to time-based releases, we no longer plan releases based on features. When a feature is ready, it is merged into the mainline and released in the following release. We never commit to a timeline, but I would say that the probability for fibers to land in one of the two releases next year as quite high. Of course, we release projects gradually, and it's possible that some planned fiber features will not land in the first release, or that the performance in the first release will later be improved etc. However, early access Loom binaries (with API still very much in flux) are expected in a month or so, with the intention of gathering feedback on the API. The decision on when fibers are ready to be merged will greatly depend on that feedback.
The OS one has more _requirements_ (the number of "it must do..." is much higher), which constrains it's design space much more. I think you can call that as "having more constraints" because it must meet more needs to be even viable as a solution.
The number of requirements for JVM threads is much lower, thus less constraining, allowing it more freedom to implement solutions that can meet its narrower window of features.
If you are interested in someone pontificating about this at length, see https://youtu.be/GqmsQeSzMdw
1. Fibers are more lightweight than threads because we only need to carry around a small amount of state representing the stack used so far by the fiber and a few other things. So it should be possible to have many more of them than we can have OS threads.
2. We can in theory make decisions about scheduling based on what the program is doing in ways that the OS cannot do. For example if one fiber releases a lock we might know to schedule the fiber waiting for that lock in preference to anything else, and would not have to wake all fibers waiting on that lock and let them race to acquire it as commonly happens with OS threads.
3. Some of these advantages can be done with OS threads when combined with user scheduling. This is available in Windows, but has not made it into mainline Linux yet. It allows the application to give the OS useful hints on which thread to schedule next, but it doesn't really reduce the weight of the threads themselves.
(I work for Oracle, and spend some of my time on project loom. These opinions are my own.)
That is not how OS level threads would work. When a lock is released, the next ready-to-run task (blocked on that lock) will be made runnable. They wont all be released then 'race' to acquire the lock. The behaviour is identical in user-space or kernel-space.
Spin-locks are not really the mechanism of choice for this level of abstraction.
I imagine Java can do some party tricks with lock sharing too with fibers backed by the same OS thread.
I'm kidding, his posts are awesome :)
Yeah, if just someone had banned him, we could have had Loom by now.
Just kidding as well... great work pron
Jersey is written in a continuation passing style. That's the only time I've seen the style outside academic discussions of Scheme. I found the code flow difficult to grok and thought continuation passing didn't fit Java very well. That may all be just due to my lack of experience, though.
Granted, the alternatives to handling concurrency each have their own problems. Maybe this is a good idea; it will be interesting to see in practice.
It's kinda of funny how languages are slowly becoming more and more Lisp-like — even Lisp is available, still offering what it offers.
As an aside: argh, in Firefox it stole the spacebar, so there's no way to scroll down the page, even if I click outside of the video.
Why do web pages do that?
I also remember reading somewhere that this wasn't possible to add/not on the road map for the JVM. Does project Loom change this?
The way I read it, tail call elimination is the guaranteed application of tail call optimisation with a well defined meaning of what a tail call is. Eg scheme requires TCE.
Tail call optimisation is a transformation a compiler may choose to do to make code that looks like “return foo(...)” run faster. A compiler may choose to not do it because it might not be implemented or faster or it could make debugging harder. There is no guarantee it will happen.
TCO makes some code faster. TCE allows one to write different looking programs knowing they won’t blow up the stack.
This all being said I think most people mean what I have referred to as TCE when they say TCO.
Yes, the elimination of the tail call is the optimization.
FWIW, Clojure handles this through syntax, so recursive algorithms are generally implemented with loop/recur forms that don't blow up the stack (instead of having a function literally call itself): https://clojuredocs.org/clojure.core/loop
We were not out to win over the Lisp programmers; we were after the C++ programmers. We managed to drag a lot of them about halfway to Lisp.
Our server has long waiting http handlers, which occupy java threads while waiting, thus limiting the server throughput to the number of threads java can maintain, and I'm holding off a rewrite on async servlets for several years already to avoid complicating the code, because I hope cheap threads (fibers) will become available, but there is no timeline for Loom. When can we hope it will be released?
That's not necesserily a deal breaker, but that's the difference.
For Java there is Quasar lib, which introduces async/await through bytecode instrumentation. It's by the same authors who work on the project Loom currently.
I don't use it because: a) the project web site feels a bit abandoned; I guess they concentrate on Loom now 2) I need analogues of wait/notify, sychronized, etc which are used in this code and not provided by Quasar; Quasar only provides lower-level primitives. One either need to implement such constructs on top of Quasar, or reimplement the logic. So it's not a drop-in replacement.
The project Loom aims to be mostly a drop-in, in particular fibers will support synchronized, wait/notify, etc.
However, early access Loom binaries (with API still very much in flux) are expected in a month or so, with the intention of gathering feedback on the API. The decision on when fibers are ready to be merged will greatly depend on that feedback.
[0] https://docs.racket-lang.org/distributed-places/index.html
Are they now trying to make Java to work like PL/SQL, Cobol, SAP (ABAP = SQL+COBOL)..?
I think it is a good idea, and is a way to bridge the gaps between the old and “new” way of writing programs. The real reason is not just scalability but traceability, that you can change code at runtime, closer connected to the database..
http://web.archive.org/web/20190127111220/https://wiki.apach...
From the Rhino Google group, it looks like some people are still using continuations in Rhino:
https://groups.google.com/forum/#!topic/mozilla-rhino/Gfo-dO...
In a Haskell context, since continuations are typically implemented as a monadic value, the statement would be more or less nonsensical. But I don't think that would be the context in question.
Edit: noelwelsh speaks of another level of composition than I was, where one is trying to take, say, State and STM and create a new monadic value that does both those things. The analysis given there is correct as well. My point here is more syntactic and basic functioning, in that even if you pick a single monadic value type to stick with, it's not going to play well in Java anyhow. So, in conclusion, generalized monad-type code in Java is just comprehensively not really possible. Continuations, or what are being called that in modern times, however, fit into Java-type languages, as proved by several very similar languages that have had them for many years, and they work fine.
Having said that, monads don't compose well regardless of language.
Now I disagree with Ron's assertion that continuations compose better than monads. Here's why:
All monads can be expressed in terms of the continuation monad. As the name suggests this is just a monadic encoding of continuations. (See https://www.schoolofhaskell.com/school/to-infinity-and-beyon...).
If your language has continuations built-in then all programs are effectively running in some "ambient" monad that can express all other monads. Just like if you have exceptions you're effectively running within an error-handling monad.
So essentially rather than writing `F A` (or `F[A]` or `F<A>` or whatever syntax your language supports) to represent effectful programs all programs are implicitly running within such an effect type.
Clojure already supports forms of delimited continuations. You can use core.async for example, or cloroutine. That said, it can't yield across stack frames. One problem with that is that if you use a go block for example, calling anything inside it which will block can sabotage your go thread. With Project Loom, all blocking call made within a Fiber will yield it instead of blocking the thread. That would be the big advantage, and possibly something Clojure could leverage as well, so that say making a blocking IO call even indirectly inside a go block would park instead of blocking.
Now if we are speaking about coroutines, there are other languages even older than Modula-2, although it was probably the most mainstream one at its peak.
I remember from back in the Scheme R5RS era there was a conflict between call/cc and dynamic-wind.
If you have a dynamic extent (like for example a try {} finally {} block that does resource allocation/deallocation), and you exit, but then jump back into the block, how far do you try to re-wind the state? You probably don't re-open files, but you would probably want to re-set any dynamic-scoped variables.
I don't remember seeing this raised as an issue recently, so it must have been solved somehow, and I'm just wondering how.
However, you might want to look up delimited continuations as a different approach to similar kinds of control flow.
Someone (I forget who) had a critique about dynamic-wind itself that (IIRC) involved capturing continuations from within the before- and after-thunks of dynamic-wind, but that seems more like an edge case.
https://kotlinlang.org/docs/reference/coroutines-overview.ht...
The difference is very big from the programmer's perspective. Kotlin's coroutines are a syntactic concept; i.e. a piece of code is either a subroutine or a coroutine, and you can use one or the other in different syntactic contexts. Loom's continuations, however, are a purely dynamic construct, like a thread. You can run any piece of code inside a continuation. The implication is that no language change is required for continuations and the fibers that are based on them -- they are just a different implementation of threads -- and no API changes. In fact, even though this talk provides some background, developers don't need to learn about continuations at all to use them. All they need to know is that they can use a light-weight implementation of threads.
Thanks for answering though, I look forward for this. I shared it accross a number of dev communities I frequent and one joke was "java and lightweight in the same sentence" which made me chuckle since I know Fibers are amazing.
They're changing the JVM and Java specs, so what is forbidden or not will change.
Each time you resume the execution of a fiber, it continues to execute until it reaches its next yield point. There is no way to go "back in time" and resume execution from a previous state of the fiber, which is what multishot coroutines or the fork system call would let you do.
Oleg Kiselyov has written about it extensively: http://okmij.org/ftp/continuations/
The intro music's a little weird for a dev con.
Even do a simple interpreter with them quickly get out of control.
Exist update info in how tame it?
https://www.reddit.com/r/ProgrammingLanguages/comments/9r2kt...
but still look interesting if somehow manage to be performant-enough and tame enough. I think continuations are best for the internals of the compiler/vm than to surface to the end user...
This is interesting, any idea of how they can do it?
So whilst I haven't looked at the code, presumably continuations are pre-empted by forcing the host thread to a safepoint and then unmounting it.
Note that forced pre-emption + a scheduler + a userspace notion of a thread gives you the tiniest core of an operating system. The next big leap in operating system design is certainly language-generic virtual machines fused with the basics of an OS.
This is especially widespread with spring boot. I've even read sentences like "It's hard to say what it does, you can only see it's effects!"
If I look at Go code, for example, I can hardly bear all the tedious repetition and dumb code! There is hardly any magic in Go, but the downside is, that you have to write every `filter`, `map` and `reduce` by hand.
It isn't particularly balanced to assume that the spring way is the only way to do things because magicians...
If the OP has that problem with spring then he can use the base servlet API or another solution.
But now I have to support multiple Spring boot projects. I can't help noticing one thing common in these projects that it is about 10% functionality and 90% of Spring turd nuggets strewn all over project repos.
I'll add that autoconfiguration is a symptom, not a solution.
That's what I thought the optimal route was. That's how much we benefit from the strong spring ecosystem.
(It was a little less of a pain than you might think because I didn't have to support all spring features, just the ones we use, which happen to mostly not be features used during singletons preinstantion)
With the caveat that there are JVMs written in Java, so that line can be crossed:
The same mechanism could apply at the Spring Boot boundary. Using Spring Boot (for simple applications) is easy enough. Implementing it takes code that is beyond bizarre.
The ability to swap out basically any component of a framework like Spring simply by including a different dependency or replacing a bean at runtime allows for fine grained tuning that isn't possible in many other frameworks.
But this flexibility and/or modularity is enabled by a heap of pretty magical abstractions that glues everything together.
I think that's a good way to characterize it.
This is the opposite emphasis to, say, the Ruby community, which values a simple, lickable, surface interface with a large amount of opaque magic hiding behind it.
You just need to master SFINAE, ADL, template metaprogrmming including tag dispatch, constexpr and eventually you will reach C++ Gandalf status.
The only exception to this is dependency injection via annotations but for god’s sake use it in moderation.
But then I still prefer to write for loops rather than streams because I know what the jit compiler is doing.
Even the assembly language programmer is running on top of the OS. The OS is deep magic: runtime interrupts happening to make the floating point hardware handle certain edge cases correctly. Fork system calls that semantically provide a copy of the parent’s address space in almost no time by using virtual memory tricks (copy on write pages). Virtual memory itself.
The OS lets the user space assembly language programmer touch some of the hardware directly, but not all of it.
[1] https://en.m.wikipedia.org/wiki/Turtles_all_the_way_down
Yes, plenty of magic.
And then I look at biological systems, not even the brain yet, just biochemical pathways. Also lots of structure, mixed in with lots of... mixing, and lots of molecules mix and sometimes match. The whole thing is much slower than man-made electronics - but many orders of magnitude more parallel. And probabilistic. And I wonder how to go from programming the man-made stuff to describing those machines in a similar way. Right now we are only tinkering at the far edges of it. Whenever somebody posts about a revolutionary new programming languages I think to myself, you are still just working with the exact same machine. It's like when you zoom in to a flat surface, that under a microscope looks ragged. The only reason we see a huge difference between various programming languages is because we are waaayyyyy zoomed into one paradigm.
In a biological system the elements doing the work are many different molecules with distinct shapes. In CPUs there only are shapeless electrons, so unlike the molecules which meet and match all the time based on probabilities the electrons don't actively contribute to the computing themselves. Also, the molecules are almost all active, all the time, in computers our "code" lies dormant and waiting in pools (memory) doing nothing most of the time. In comparison, only an incredibly tiny fraction of our human-made computing elements are actually doing something at any point!
In nature/biology shape and structure on the nano level are the major design factor. Shapes of molecules and shapes of the structures used to separate or guide or hold them. Most of the process is determined by those shapes. In human-made computing we are extremely limited in what shapes we use under the hood. We use higher frequencies and limiting ourselves to few goals, otherwise nature would outcompute us by many orders of magnitude (and that's only true because we decide to ignore most of the computation going on in biological systems as not relevant "it's just random noise with no purpose"). I see a disconnect between how we program and what is going on. We think it's a vital "abstraction". I think that abstraction is a blessing, sure, but also a great hindrance. In the end those shapes and structures matter a lot. If somehow we could get a translation into more and more flexible (nano) shapes/structures than now, where we have a 100% fixed structure and only electrons (so, no shapes)... even biological systems are actually quite limited in what kinds of shapes they use, there was a path-dependency on which random path evolution took. I think it would be possible to far exceed biological systems, but we would have to get down to the shapes (molecules and nano-structures). That is far away from now, where it takes us years to come up with one fixed structure and shapeless moving point-forms inside of it.
Dependency injection comes in two varieties: constructor injection and future pain injection.
Choose wisely.
https://www.reddit.com/r/java/comments/7ulmwn/coroutines_in_...
It's quite possible to make non-magical coroutines in Java. It's about how you document them (and perhaps how you try to hide implementation details).
It's just the opposite, Java programmers are doing something much more limited. Java has a deliberately simplified language design, but the result is that most serious Java systems have to use one or more frameworks that step outside of what's possible in Java proper (by using reflection, proxies, bytecode manipulation, JVM agents or other such shenanigans) to meet their requirements. And such frameworks are necessarily "magic" from the perspective of someone thinking in Java proper.
This room is called Spring MVC and it has 2 million switches which will let you do anything you please. Good luck.