The main capability provided by Quasar is the fiber, or the lightweight thread. It is the same as a normal Java thread in the sense that it can block – on IO, on a DB call, or on a synchronization mechanism. This makes the programming experience very natural. The actor and the channel abstractions build upon fibers.
Akka doesn't have lightweight threads at all. You implement a message-handling method, but it must not block on, say, a DB call, lest it block the entire thread it runs in. An Akka actor simply must not issue a DB call: it's as simple as that.
With Quasar things are different: you pull messages rather than implement a callback; you can block: on IO, DB, lock or anything else. The programming then is not only simpler, but also more powerful. For example, Quasar supports selective receive - just like Erlang.
That's untrue. With Akka's pipe pattern, you can take the results of any future and pipe it back to the sender, including using a map on the future if you like. This is how we do reactive database calls in Akka. For example:
def receive = {
case msg => {
val f = future { myDatabaseResult(msg) }
f map { result => myTransformResult(result) } pipeTo sender
}
}
At no point does this actor block. Assuming you even have something like Play calling this actor, you wouldn't be blocking there, either, you'd take the result from the actor, likely map it to a result, and Play would asynchronously return that. My basic rule is that if you're typing Await.result anywhere in your Play/Akka code, you're doing it wrong.
There are many ways to do asynchronous programming employing functional approaches. The difference is that with Quasar you can use them if you like but you don't have to. You can issue a plain-old JDBC call, and at no point will the thread block, either, but the actor will: because it's simple, familiar and intuitive. You don't need to learn so many unfamiliar patterns. You program as you normally would a single thread.
Ok, so issuing a blocking call is "simple, familiar and intuitive". Invoking a Future or a Promise is "so many unfamiliar patterns".
Yes Sir, with this attitude I hope to make a remarkable progress in my tech career :) Seriously, there is nothing mysterious or magical about shoving a "plain-old JDBC call" into a Future.
> Ok, so issuing a blocking call is "simple, familiar and intuitive". Invoking a Future or a Promise is "so many unfamiliar patterns".
You got it! Great job!
> Yes Sir, with this attitude I hope to make a remarkable progress in my tech career :)
Well one way to not make a remarkable progress in your career is use fads, acronyms, and unnecessarily complicated constructs. Why use futures in that example when actors perfectly model the problem domain? Are you showing off that you know about Futures and they are easy?
I think you're pretending that removing the ceremony of the Future isn't a significant difference. Your point is correct, but you're missing the forrest for the trees
A future is a simple blocking mechanism. The Scala example above uses something called a future, but isn't. It's an "Rx" functional future – cool and often useful, but it's yet another construct that isn't part of the actor model. I'm happy to use Rx, but I wouldn't use it in an actor.
There are many ways to tackle concurrency, but IMO it's best to keep them separate as much as possible, or you quickly lose track of what's happening when.
Well he is kind of right. I'm familiar with enterprise IT, and there are very mediocre programmers at work. I'll bet you most of them have never heard of "futures". Sad, but thats how I experienced it.
I am also familiar with snobby wannabees functional programmers who instead of opening the goddam file and reading it are creating homomorphic endofunctors wrapped in futures with double memoization and distributed locks, so that nobody on the fucking team knows what's going on.
These people are 10x more dangerous than mediocre programmers who just find the simplest way to get the work done and ship the product.
Eventually 1% of the wannabes might get enlightened and realize that simple basic code is usually better than using every single programming concept wrapped in 100 lines of code that nobody (including themselves 2 weeks later) can understand.
That's quite a feat of mind-reading you performed there. The fascination with technology rather than just to solve the problem at hand via the shortest critical path is a thing that has been puzzling me for a long time. At some level technology is so fascinating in its own right that the temptation to lose sight of the goal is ever present and many people succumb to that temptation.
Imo it's just another variation on the Yak Shaving theme with a dose of procrastination thrown in for good measure.
“Well, Mr. Frankel, who started this program, began to suffer from the computer disease that anybody who works with computers now knows about. It's a very serious disease and it interferes completely with the work. The trouble with computers is you play with them. They are so wonderful. You have these switches - if it's an even number you do this, if it's an odd number you do that - and pretty soon you can do more and more elaborate things if you are clever enough, on one machine.
After a while the whole system broke down. Frankel wasn't paying any attention; he wasn't supervising anybody. The system was going very, very slowly - while he was sitting in a room figuring out how to make one tabulator automatically print arc-tangent X, and then it would start and it would print columns and then bitsi, bitsi, bitsi, and calculate the arc-tangent automatically by integrating as it went along and make a whole table in one operation.
Absolutely useless. We had tables of arc-tangents. But if you've ever worked with computers, you understand the disease - the delight in being able to see how much you can do. But he got the disease for the first time, the poor fellow who invented the thing.”
― Richard P. Feynman, Surely You're Joking, Mr. Feynman!
> The fascination with technology rather than just to solve the problem at hand via the shortest critical path is a thing that has been puzzling me for a long time
Exactly. And fascination with technology is important, it is what keeps people learning and searching, finding better tools. The problem with it, is it has a pathological side.
Like the tool analogy, just because I found an experimental, electronic, automatic nail gun, voice activated, with blinking lights, doesn't mean I should use it when building my own house, if all I need is to hammer a few nails, a regular trusted hammer will do.
I fully agree, thats why I don't like Scala. Readability and simplicity above everything.
That doesn't mean that you shouldn't be familiar with the basic concepts of the programming language that you use, and I feel the silicon valley bubble is sometimes unaware that not all programmers are startup hot shots.
You actually support my argument, introducing the concept of futures is just an added layer of unnecessary "cleverness" that could be avoided with quasar/pulsar.
Certainly not. I was just responding to dxbydt , who was questioning the statement that futures and promises are unfamiliar to many programmers, and I think they are, because I've seen some corporate IT departments from the inside, and its just a different environment.
In no way do I endorse the use of futures or whatnot, nor are they my "favourite programming paradigm".
I am simply advocating the simplest solution that works, and avoiding constructs like promises/futures seems like a good idea in that regard.
You block the lightweight thread (fiber), rather than the OS thread. Fibers are implemented as continuations scheduled by a very good multi-threaded scheduler (ForkJoinPool).
When a query is executed with JDBC, the execute() method does not return until the the DB responds. The method must have been invoked in some OS thread by quasar scheduler. Wouldn't that thread block as long as execute() doesn't complete?
To put it differently, can I make 10000 concurrent HTTP requests (to different domains), using a non-NIO HTTP client library, without ending up with one OS thread per request?
> can I make 10000 concurrent HTTP requests (to different domains), using a non-NIO HTTP client library, without ending up with one OS thread per request?
No. We provide you with a standard HTTP client API (JAX-RS client) that gives you a blocking API. Under the hood it uses asynchronous IO. We then transform callbacks to fiber-blocking operations. So you use a standard blocking API, that is implemented asynchronously.
JDBC is a little more complicated as there is no async JDBC standard. What we do, then, is run the thread-blocking call in a separate IO workers pool. Those worker threads will block, but your API call will just block the fiber, letting other fibers use the same OS thread for something else until the JDBC call completes, at which point the IO worker will wake up your fiber.
Does there need to be a handw-written integration to every kind of blocking resource, or is there magic happening here?
What happens if a thread loads from a MappedByteBuffer, and the OS needs to read from disk to satisfy the load? What happens if a thread loads from some far corner of main memory that has been paged out, and the OS needs to read from disk to satisfy the load?
Those situations go beyond the power of lightweight threading systems i have seen before. They're not fatal problems, nor even serious ones for most programs, but they're part of the reason that lightweight threading hasn't, and can't, become the general solution to threading. Well, not until scheduler activations make a comeback, at least. That doesn't mean that lightweight threading is not a hugely valuable thing to have on an opt-in basis, though, as this demonstration, er, demonstrates.
> Does there need to be a handw-written integration to every kind of blocking resource, or is there magic happening here?
Yes (to handwritten integration), but it's incredibly simple. We can transform any callback-based API to a fiber-blocking API within hours of work.
> What happens if a thread loads from a MappedByteBuffer, and the OS needs to read from disk to satisfy the load? What happens if a thread loads from some far corner of main memory that has been paged out, and the OS needs to read from disk to satisfy the load?
The thread will be blocked, but it isn't likely to be much of a problem. It's perfectly OK for the fiber to block the thread occasionally (hopefully rarely) – the work-stealing scheduler can deal with that because it runs in a thread-pool; if one thread blocks other will steal its work and do it. It's just not OK for fibers to block their thread very often. The scenarios you've described involve missed caches and so are rare by design.
> Those situations go beyond the power of lightweight threading systems i have seen before. They're not fatal problems, nor even serious ones for most programs, but they're part of the reason that lightweight threading hasn't, and can't, become the general solution to threading.
I agree. Quasar fibers are by no means meant to serve as a replacement for threads. They are specifically targeted for cases when you want lots of concurrent "threads" that interact very often by passing information (either via messages or a shared data structures), and so block and wait for each other a lot.
If you have a long-running computation: use a plain thread.
Actually, one of the cool things in Quasar is an abstraction called a strand. A strand is simply either a thread or a fiber. All of the synchronization mechanisms (channels, condition variables etc.) provided by Quasar work with strands - not directly with fibers -
so you can use them both in fibers or threads. In fact, you can run a Quasar actor in a thread rather than a fiber.
Every fiber-blocking operation eventually ends up with a call to Fiber.park(). If you want to call park in your function or call a function that calls park etc., you need to let Quasar know that your function is "suspendable". There are several ways to do that: you can declare to throw a SuspendExecution exception, you can annotate your method with a @Suspendable annotation, or you could declare it as suspendable programmatically or in an external text file.
Correct, but we'll provide implementations for the most popular IO APIs (NIO, REST services, web sockets, JDBC etc.) so it is a limitation, but I think it's a small one.
Once the documentation is complete you'll know how to transform any callback-based asynchronous call into a fiber-blocking one, so you could integrate your own libraries with Quasar. It's very-very simple.
Scala has an `async`/`await` feature (like C#) now, which hides the Future ceremony and gives sequential syntax. https://github.com/scala/async
I guess this does still require the `await` word, but I think it's good to have a magic word for "suspend execution now" so you can see where it's happening.
An OS thread still has to block on blocking IO calls somewhere, of course. There's no "syntactic" fix for that on the JVM - you have to actually port the blocking IO to nonblocking IO - AFAIK nobody can magically fix JDBC to be nonblocking from outside JDBC.
Essentially, Quasar provides async and await for all JVM languages. async is called `Fiber.start()`, and await is called `Fiber.park`.
Other than working for all JVM languages, Quasar fibers are more general in that they can spawn many functions (they have a stack), while async is limited to a single expression block. Because of this, we can hide the "await" deep inside the JDBC call stack.
Under the hood, they are similar: both instrument your code. Only async does this at the language level (it's a Scala macro), while fibers do it at the bytecode level.
> Akka doesn't have lightweight threads at all. You implement a message-handling method, but it must not block on, say, a DB call, lest it block the entire thread it runs in. An Akka actor simply must not issue a DB call: it's as simple as that.
That's not quite true. Yes, you block that thread but this is why you configure actors with blocking calls to use their own dispatcher. For DB queries you typically put querying actors onto a thread-pool dispatcher where pool size ~= available DB connections.
Selective receive is also quite easy to implement in Akka using stash().
I am really looking forward to where actor frameworks like Akka are going, but that seems like a leaky abstraction to me.
I shouldn't have to define extra actors or figure out thread pool concurrency levels to do blocking operations like lock acquisition, waiting on conditions, IO etc. A framework that that doesn't allow that adds zero value to me because I am not willing to ask others to reason about that sort of thing.
Not having that kind of transparency makes it difficult and dangerous to convert existing code bases that use traditional concurrency primitives.
I would like to have thousands or millions of actors, but for now I am stuck going to 1:1 with threads.
I agree. That's why Quasar starts by providing true lightweight threads - you can block, wait on conditions - whatever. Only, you can have millions of those.
On top of these fibers, Quasar gives you Go-like channels and/or Erlang-like actors (I say Erlang like because they follow the Erlang model closely: you pull messages rather than implement message callbacks, they have selective receive etc.)
Quasar looks very cool. How does pre-emption work? I know Erlang's VM count an actors' "reductions" -- bytecode instruction and after a certain number preempts that actor and lets other run. How does that work in Quasar? Does an actor have to explicitly yield, sleep, do IO or run receive?
We've experimented with reduction-based preemption but saw no perceivable performance benefit (you can look at the Fiber class code and see them commented out). We might bring it back if we find a good use for it.
But, at least for the time being I wouldn't run a long-running, CPU heavy computation in a fiber, but in a plain thread. Fibers work best when they block often.
And yes, you can configure Akka for certain types of usage, but it is anything but simple. We value the simplicity and intuitiveness that are at the core of Clojure and Erlang.
I didn't say that stash() is the same, just that it's quite easy to use it to implement selective receive in Akka in combination with hot-swapping an actor's behavior with become/unbecome. We use this pattern quite successfully in our code base.
That is not a correct assumption. Most DB vendors only provide JDBC drivers. There are some async drivers, but there is no standard for them and availability and quality varies substantially. This problem is repeated for other network libraries - if they weren't written specific for NIO then you have to KNOW that and you have to be sure you run that Actor in it's own thread. Its a leaky abstraction.
How does the Quasar know when it should defer execution of something that might block? Does the user declare this in some fashion? In the article, it looks like this is done with try-with-resource blocks.
The try-with-resource block is used to delineate an atomic transaction, and is part of the SpaceBase API. An example in the code for a blocking call is the call to receive.
Quasar identifies blocking methods if they declare that the throw a SuspendExecution exception, marked with a @Suspendable annotation, or listed in an external file. Pulsar, Quasar's Clojure API, marks suspendable functions differently, but that's an implementation detail.
Akka doesn't have lightweight threads at all. You implement a message-handling method, but it must not block on, say, a DB call, lest it block the entire thread it runs in. An Akka actor simply must not issue a DB call: it's as simple as that.
With Quasar things are different: you pull messages rather than implement a callback; you can block: on IO, DB, lock or anything else. The programming then is not only simpler, but also more powerful. For example, Quasar supports selective receive - just like Erlang.