The Parallel Universe Blog

May 21, 2015

Quasar and Akka – a Comparison

By Fabio

The actor model is a design pattern for fault-tolerant and highly scalable systems. Actors are independent worker-modules that communicate with other actors only through message-passing, can fail in isolation from other actors but can monitor other actors for failure and take some recovery measures when that happens. Actors are simple, isolated yet coordinated, concurrent workers.

Actor-based design brings many benefits:

  • Adaptive behaviour: interacting only through a message-queue makes actors loosely coupled and allows them to:
    • Isolate faults: mailboxes are decoupling message queues that allow actor restart without service disruption.
    • Manage evolution: they enable actor replacement without service disruption.
    • Regulate concurrency: receiving messages very often and discarding overflow or, alternatively, increasing mailbox size can maximize concurrency at the expense of reliability or memory usage respectively.
    • Regulate load: reducing the frequency of receive calls and using small mailboxes reduces concurrency and increases latencies, applying back-pressure through the boundaries of the actor system.
  • Maximum concurrency capacity:
    • Actors are extremely lightweight both in memory consumption and management overhead, so it’s possible to spawn even millions in a single box.
    • Because actors do not share state, they can safely run in parallel.
  • Low complexity:
    • Each actor can implements stateful behaviour by mutating its private state without worrying about concurrent modification.
    • Actors can simplify their state transition logic by selectively receiving messages from the mailbox in logical, rather than arrival order 1.

The actor model reached widespread recognition thanks to Erlang and it has successfully met its goals in critical production systems.

This is a comparative review of two actor libraries for the JVM: our own Quasar and Akka by Typesafe.

Quasar

Quasar is an open-source library for simple, lightweight JVM concurrency, which implements true lightweight threads (AKA fibers) on the JVM. Quasar fibers behave just like plain Java threads, except they have virtually no memory and task-switching overhead, so that you can easily spawn hundreds of thousands of fibers – or even millions – in a single JVM. Quasar also provides channels for inter-fiber communications modeled after those offered by the Go language, complete with channel selectors. It also contains a full implementation of the actor model, closely modeled after Erlang.

While this post is mostly concerned with Quasar’s implementation of the actor model, which is built on top of Quasar fibers, keep in mind that you can use Quasar without actors.

Quasar actors implement the full actor paradigm outlined above with some for Java 7, Java 8, Clojure and Kotlin. Quasar does not currently support Scala.

Because Quasar fibers work so much like threads, it is easy to integrate existing libraries, so that current tools and libraries can be used with no or minimal code changes while taking full advantage of lightweight threads’ efficiency. This allows preserving existing code and avoids API lock-ins. The Comsat project takes advantage of Quasar’s integration frameworks to provide fiber-enabled porting of several popular and standard APIs with minimal code (it also introduces Web Actors, a new actor-based web API for HTTP, WebSocket and SSE).

Quasar fibers are implemented by creating and scheduling continuation tasks and since the JVM doesn’t (yet) support native continuations, Quasar implements them through selective bytecode instrumentation: methods that can block a fiber currently need to be explicitly marked through annotations so that Quasar can insert the continuation suspension and resumption hooks. However, experimental automatic Clojure instrumentation is available and automatic instrumentation is going to extended to other JVM languages as well. Instrumentation can be performed either as an additional build step or at runtime (through a JVM agent or a class loader for most common servlet containers).

Akka

Akka is an actor framework written in Scala, which supports Java 7, Java 8 (experimental as of 2.3.10) in addition to Scala and the Akka.NET project offers a port to .NET and Mono. It offers a asynchronous, callback-based Actor DSL rather than an Erlang-style, fiber-based actor system. Akka doesn’t provide lightweight threads but relies on JVM threads to schedule actors. Rather than a library, Akka is a full-service framework, covering everything from configuration and deployment to testing.

Blocking Vs. Non-Blocking

A major difference between Akka and Quasar actors is that Akka uses an asynchronous, non-blocking API, while Quasar – like Erlang, Go and Clojure’s core.async – uses a blocking API: In Akka, an actor implements the receive method, which is a callback triggered when a message is received by the actor, while in Quasar the actor calls the receive method, which blocks until a message is received. From a theoretical standpoint, the async and direct (or blocking) styles are dual and equivalent because they can be transformed into each other, but in practice, the details of the implementation have a significant effect on performance and scalability, and the choice of a programming language can make one approach easier than the other.

A reason for choosing the asynchronous, callback-based approach has been that blocking plain OS threads entails significant overhead (as does the mere existence of many threads), which can be avoided with a non-blocking API. However, because Quasar – just like Erlang and Go – has true lightweight threads, blocking carries virtually no overhead.

On the language side, while Scala provides syntactic support for monads, which make dealing with asynchronous code simpler, the blocking approach is much simpler in languages that don’t have good syntactic support for monads – like Java. The advantage for blocking code is not only simpler, more readable and more maintainable Java code, but a more familiar and compatible code, that allows integrating other standard Java APIs.

An API comparison

The Quasar Java API supports Java 7 and 8. Clojure support is part of Pulsar, a thin wrapping layer around Quasar that is very idiomatic and that offers an actor API very similar Erlang’s. Kotlin support is the most recent addition. Kotlin is a very promising statically typed hybrid programming language targeting the JVM and JavaScript, designed and built to be efficient and integrable by the dev-tools leading vendor JetBrains. Kotlin makes using existing Java APIs an efficient, safer and still easier and more pleasant experience than Java itself.

Quasar also offers an integration toolkit that enables adding support for additional JVM languages.

Akka was designed mainly with Scala in mind but it has been offering an additional Java API for some time now. It can of course be used with other JVM languages as well, although it doesn’t (yet) offer an idiomatic API for them.

The following is a comparison between Quasar and Akka Java APIs. While far from exhaustive, it covers the crucial differences. Here’s a brief summary:

Features comparison

Actor definition

Quasar (Kotlin and Java) actors implement the doRun method (or a function in Clojure) that, just like in Erlang, can use arbitrary language control flow constructs and strand-blocking operations whenever the developer sees it fit; typically it will use at least receive (normal or selective) and send:

class MyActor extends BasicActor<String, MyActorResult> {
    private final Logger log = LoggerFactory.getLogger(MyActor.class);

    @Suspendable
    @Override
    protected MyActorResult doRun() throws InterruptedException, SuspendExecution {
        // ...Arbitrary code here...
        final String msg = receive(m -> {
            if ("test".equals(m)) return "testMsg";
            else return null; // Defer
        });
        // ...Arbitrary code here...
        return new MyActorResult();
    }
}

The Clojure API, provided by Pulsar, is even more concise:

(def log (LoggerFactory/getLogger (class *ns*)))

(spawn
  #(
    ; ...Arbitrary code here...
    (receive                                ; Single, fiber-blocking, selective receive
      "test" (do (. log info "received test") "testMsg")) ; Other messages will stay in the mailbox
    ; ...Arbitrary code here...
  ))

Quasar actors – like Erlang processes – use blocking receive, and are allowed to perform blocking IO (although blocking IO operations don’t end up blocking OS threads, but rather fibers, which makes them very scalable).

Akka actors are implemented as a callback to a receive event, and are not allowed to block:

public class MyUntypedActor extends UntypedActor {
  LoggingAdapter log = Logging.getLogger(getContext().system(), this);

  // "receive" must be toplevel
  public void onReceive(Object message) throws Exception {
    if ("test".equals(message))
      log.info("received test");
    else
      log.info("received unknown message")
  }
}

Actor lifecycle and supervision hierarchy

Quasar actors are created and started on a fiber as easily as:

ActorRef myActor = new MyActor().spawn();

While both Quasar and Akka support monitoring (aka watching) other actors for failure, Akka makes adding actors to proper supervision hierarchies mandatory, so actors must always be instantiated through a recipe specifying the actor’s class and appropriate constructor arguments. Top-level actors must be spawned by an Actor System and child actors by a parent’s context:

ActorRef myActor = system.actorOf(Props.create(MyActor.class), "myactor");

Supervision is a hierarchical failure management pattern that provides good practices of failure isolation: a supervisor will operate according to its supervision strategy when its children actors terminate. A supervised child actor can terminate because of permanent failures, temporary failures or because they have simply finished their jobs. When termination happens, typically supervisors can choose to fail themselves (escalation), restart only the failed child or restart all of them.

In Quasar, like in Erlang, supervision is optional, and a supervisor is just a pre-built, though configurable, actor that provides supervision by internally using the primitive actor monitoring operations (watching and linking). When using a supervisor, Quasar, too, requires specifying a recipe for actor creation (as well as additional information such as how many retries the supervisor should attempt before giving up etc.):

ChildSpec actorSpec = new ChildSpec("myactor", TRANSIENT, 1, 1, MILLISECONDS, 100, MyActor::new);
Supervisor mySupervisor = new SupervisorActor(ALL_FOR_ONE, actorSpec).spawn();

Quasar also allows supervising and restarting pre-built local actor instances through the overridable Actor.reinstantiate method, so it can work with any dependency engine.

Akka provides several ways for shutting down actors. Quasar, like Erlang, simply encourages a simple message to signify a shutdown request (though this common mechanism is already part of all behaviours – see below); abrupt termination is possible by interrupting the actor’s underlying strand (thread or fiber).

Behaviours

Quasar follows the Erlang OTP library’s example in providing configurable actor templates for common actor types called behaviours. Behaviours all implement common, useful, messaging patterns, but in Quasar they also add convenience methods to the actor’s reference. Quasar’s behaviours are all modelled after OTP:

  • An EventSourceActor (modeled after Erlang’s gen_event) can dynamically register and un-register handlers that will just react to the message they receive. If you think that this specific type of Quasar actor corresponds very closely to the reaction-only, asynchronous Akka actors, then you’re on the right track.
  • A ServerActor (modeled after Erlang’s gen_server) models a service that exposes a request-response API.
  • A ProxyServerActor allows writing interface-based servers: it is built by passing any interface implementation and will produce an ActorRef that will proxy the interface and send messages corresponding to its methods to the underlying server actor. It’s just one use case of course, but I think this behavioural actor can help a lot when porting traditional APIs to Quasar actors.
  • A FiniteStateMachineActor, added in the upcoming Quasar 0.7.0 (modeled after Erlang’s gen_fsm), makes it easy to write actors as explicit finite-state-machines.

Akka does not include pre-built actor templates of this kind. Instead, various common behaviours are built into the standard API.

Actor Systems

Akka can run as a container for standalone deployment or as a library; it is setup through configuration files referencing multiple actor systems, each led by a single supervisor. The configuration encompasses logging, scheduling (AKA dispatching), networking, messages serialization and balancing (AKA routing) for each of them. Sensible defaults are provided as well, so configuration is optional.

In Akka, an actor system is a heavyweight object and corresponds to a logical application. Quasar, being a library rather than a framework, does not have the notion of actor systems at all because it doesn’t need to encompass your entire application. Various specific configurations are, of course, possible:

  • The default scheduler for fibers is fork-join (work-stealing) but it can be chosen even per-fiber. Actors simply inherit the scheduling mechanism used for the strands they run on, which means they don’t need a scheduling/dispatching setup themselves.
  • The supervision hierarchy is optional, so there’s no need for “root” supervisors.
  • Any logging mechanism can be used, but the (optional) behaviours use the “standard” logging API SLF4J for this purpose.
  • Quasar offers networked actors and actor migration in a Galaxy cluster out-of-the-box, but can support more. Clustering features are setup in the cluster provider’s configuration (e.g. Galaxy), not in Quasar itself.
  • Quasar does not concern itself with deployment. For a cool deployment solutions for any JVM app (which also works well for applications employing Quasar), we recommend you take a look at Capsule.

Internal actor API

Quasar’s default internal API of an actor includes only the following:

  • The receive/tryReceive methods and an overridable filterMessage to discard messages before they are received.
  • The external, opaque reference to the actor.
  • Basic actor monitoring constructs link, watch and an overridable handleLifecycleMessage.

More features such as sender references embedded by default, logging, termination request handling, request serving, event-handling and supervision can be obtained by extending pre-built behaviours or can be added by you. Also, since thanks to Quasar fibers send and receive operations can be blocking and efficient at the same time, there’s no need for asynchronous, Future-returning send variant such as Akka’s ask.

Akka features such as monitoring and supervision are always enabled for all actors, so the internal API is extensive:

  • The receive method.
  • The external, opaque reference to the actor.
  • A reference to the last message sender (if any).
  • Overridable lifecycle methods.
  • The supervisor strategy in use.
  • A context property with additional facilities such as:
    • Factory methods to create supervised children.
    • A reference to the actor system owning the actor.
    • The parent supervisor.
    • The supervised children.
    • Basic actor monitoring (“DeathWatch”) methods.
    • Hot-swap (AKA “become”) facilities.

Akka also offers an optional Stash trait that enables managing a second queue of messages that have been received but whose processing should be delayed. In contrast, Quasar, like Erlang, allows for selective receive, so it doesn’t require the developer to manage additional message queues just for the sake of delaying message processing.

Hot-upgrade

Quasar allows full and automatic upgrade of actors at runtime, by loading new classes through JMX or a designated “module” directory. Quasar also allows upgrading an actor’s state in a controlled fashion through methods annotated with @OnUpgrade.

Akka supports swapping an actor’s partial function with a new one at runtime though the become method, but offers no support for class redefinition, so either actor behaviour must be replaced with bytecode already present in the running JVM or new code must be loaded through some other tool.

Networking, remoting, reliability and clustering

Quasar supports remote actors out-of-the-box as part of a clustering ensemble on top of Galaxy but more remoting and clustering providers can be added. Akka provides similar abilities plus the pre-built abilities to directly spawn an actor on a remote node and load-balance messages among actors on separate nodes.

Quasar also experimentally supports actor migration – the ability to suspend a running actor and resume it on another machine.

Mailbox persistence

Akka includes experimental mailbox persistence support based on its underlying event sourcing engine and requires an actor to extend the PersistentActor trait and to provide two separate event handlers for normal behaviour and recovery, as well as explicit calls to persist.

Quasar at present doesn’t ship with support for actors mailbox persistence.

Integration

Quasar doesn’t force a developer into using all the features of an actor system, nor into using actors at all. In fact, Quasar offers an easy-to-use integration framework for 3rd-party technologies featuring either async, future-based or blocking APIs, so that they can be used with lightweight threads (“fibers”) rather than regular heavyweight ones. Quasar allows you to freely mix actor and non-actor code, or use any integrated library from within actor code with no need for a specialized API.

Comsat uses this framework to integrate standard and popular Java and Clojure technologies:

Comsat also include Web Actors, a new actor API to handle HTTP, WebSocket and SSE exchanges.

Currently the Akka project offers:

  • Apache Camel messaging integration.
  • HTTP actor-based API (Spray).
  • ZeroMQ actor-based API.
  • TCP actor-based API.
  • UDP actor-based API.
  • File IO actor-based API.

Akka integrations with systems not based on message passing are necessarily new actor APIs.

Testing

Quasar doesn’t include a dedicated testing kit because it is a blocking framework with support for temporary actors whose strand can produce a value upon termination, so regular testing tools like JUnit can be adopted together with regular multi-threaded testing practices.

Akka is an asynchronous framework, so it has to offer dedicated APIs in the form of blocking single-actor testing calls (TestActorRef, TestFSMRef). It also provides special ScalaTest assertion-enabled actors to perform external integration testing of whole actor subsystems (TestKit mixin or TestProbes). There is support for timing assertions, supervision testing, message exchange throttling, message exchange and failure tracing.

System monitoring and management

Quasar exposes rich actor monitoring data (mailbox, stack trace) via standard JMX MBeans which can be monitored through JMX-enabled tools, such as JDK’s freely available JVisualVM and JConsole, or with a REST API using Jolokia. In addition, Quasar provides tools to fine-tune instrumentation and to record detailed fiber execution traces.

Akka applications can be monitored and managed through proprietary software (Typesafe Console), which requires a commercial license for production systems. Akka Cluster has support for JMX monitoring.

New Relic and App Dynamics support Akka as well as Quasar (through JMX).

Full-app comparison: Quasar Stocks and Reactive Stocks

There is no better way to understand the similarities and differences between Akka and Quasar, than to look at the code for an identical application written using both. Quasar Stocks is a Java port of the Reactive Stocks Play/Akka activator template to Quasar actors and Comsat Web Actors.

At 385 lines of code, the pure-Java Quasar application is close to be as compact as the half-Scala Typesafe one (285 l.o.c.) and this is especially good considering actors and Web Actors do just one thing well: everything is conf- and JSON-library agnostic so you’re not forced into using just one web framework and to accept its opinions about web development matters.

And still I think the Quasar one is simpler to understand because it is plain-old Java imperative style, only running on a much more efficient lightweight threads implementation: no declarative/functional/monadic/async is forced down your throat just to work around JVM threads’ heavy footprint.

For example the “Stock Sentiment” future-based web services in the Typesafe version can be replaced with an equally efficient and completely traditional JAX-RS Jersey version, only fiber-blocking instead of thread-blocking. So instead of using asynchronous operations Futures and a dedicated, non-standard DSL to compose them, like in the Typesafe version:

object StockSentiment extends Controller {
  case class Tweet(text: String)

  implicit val tweetReads = Json.reads[Tweet]

  def getTextSentiment(text: String): Future[WSResponse] =
    WS.url(Play.current.configuration.getString("sentiment.url").get) post Map("text" -> Seq(text))

  def getAverageSentiment(responses: Seq[WSResponse], label: String): Double = responses.map { response =>
    (response.json \\ label).head.as[Double]
  }.sum / responses.length.max(1) // avoid division by zero

  def loadSentimentFromTweets(json: JsValue): Seq[Future[WSResponse]] =
    (json \ "statuses").as[Seq[Tweet]] map (tweet => getTextSentiment(tweet.text))

  def getTweets(symbol:String): Future[WSResponse] = {
    WS.url(Play.current.configuration.getString("tweet.url").get.format(symbol)).get.withFilter { response =>
      response.status == OK
    }
  }

  def sentimentJson(sentiments: Seq[WSResponse]) = {
    val neg = getAverageSentiment(sentiments, "neg")
    val neutral = getAverageSentiment(sentiments, "neutral")
    val pos = getAverageSentiment(sentiments, "pos")

    val response = Json.obj(
      "probability" -> Json.obj(
        "neg" -> neg,
        "neutral" -> neutral,
        "pos" -> pos
      )
    )

    val classification =
      if (neutral > 0.5)
        "neutral"
      else if (neg > pos)
        "neg"
      else
        "pos"

    response + ("label" -> JsString(classification))
  }

  def get(symbol: String): Action[AnyContent] = Action.async {
    val futureStockSentiments: Future[Result] = for {
      tweets <- getTweets(symbol) // get tweets that contain the stock symbol
      futureSentiments = loadSentimentFromTweets(tweets.json) // queue web requests each tweets' sentiments
      sentiments <- Future.sequence(futureSentiments) // when the sentiment responses arrive, set them
    } yield Ok(sentimentJson(sentiments))

    futureStockSentiments.recover {
      case nsee: NoSuchElementException =>
        InternalServerError(Json.obj("error" -> JsString("Could not fetch the tweets")))
    }
  }
}

It is possible to write a completely standard, familiar JAX-RS service, the only difference being the additional @Suspendable annotation and spawning fibers rather than threads for parallel operations:

@Singleton
@Path("/")
public class Sentiment {
    final CloseableHttpClient client = FiberHttpClientBuilder.
            create(Runtime.getRuntime().availableProcessors()).
            setMaxConnPerRoute(1000).
            setMaxConnTotal(1000000).build();

    @GET
    @Path("{sym}")
    @Produces(MediaType.APPLICATION_JSON)
    @Suspendable
    public JsonNode get(@PathParam("sym") String sym) throws IOException, ExecutionException, InterruptedException {
        List<Fiber<JsonNode>> agents = new ArrayList<>();
        List<JsonNode> sentiments = new ArrayList<>();
        for (JsonNode t : getTweets(sym).get("statuses"))
            agents.add(sentimentRetriever(t.get("text").asText())); // spawn worker fibers
        for (Fiber<JsonNode> f : agents) // join fibers
            sentiments.add(f.get());
        return sentimentJson(sentiments);
    }

    private JsonNode sentimentJson(List<JsonNode> sentiments) {
        Double neg = getAverageSentiment(sentiments, "neg");
        Double neutral = getAverageSentiment(sentiments, "neutral");
        Double pos = getAverageSentiment(sentiments, "pos");

        ObjectNode ret = Application.Conf.mapper.createObjectNode();
        ObjectNode prob = Application.Conf.mapper.createObjectNode();
        ret.put("probability", prob);
        prob.put("neg", neg);
        prob.put("neutral", neutral);
        prob.put("pos", pos);
        String c;
        if (neutral > 0.5)
            c = "neutral";
        else if (neg > pos)
            c = "neg";
        else
            c = "pos";
        ret.put("label", c);
        return ret;
    }

    private Double getAverageSentiment(List<JsonNode> sentiments, String label) {
        Double sum = 0.0;
        final int size = sentiments.size();
        for (JsonNode s : sentiments)
            sum += s.get("probability").get(label).asDouble();
        return sum / (size > 0 ? size : 1);
    }

    private Fiber<JsonNode> sentimentRetriever(String text) throws IOException {
        return new Fiber<> (() -> {
            HttpPost req = new HttpPost(Application.Conf.sentimentUrl);
            List<NameValuePair> urlParameters = new ArrayList<>();
            urlParameters.add(new BasicNameValuePair("text", text));
            req.setEntity(new UrlEncodedFormEntity(urlParameters));
            return Application.Conf.mapper.readTree(EntityUtils.toString(client.execute(req).getEntity()));
        }).start();
    }

    @Suspendable
    private JsonNode getTweets(String sym) throws IOException {
        return Application.Conf.mapper.readTree (
            EntityUtils.toString(client.execute(new HttpGet(Application.Conf.tweetUrl.replace(":sym:", sym))).getEntity()));
    }
}

The blocking style has another benefit: the Quasar API is smaller and simpler. For example Akka’s specific support for scheduled messages is not needed at all, because in Quasar the actor body can use regular control flow constructs. So instead of:

// Fetch the latest stock value every 75ms
val stockTick = context.system.scheduler.schedule(Duration.Zero, 75.millis, self, FetchLatest)

A regular fiber-blocking timed receive within a message processing loop is more than enough:

for(;;) {
    Object cmd = receive(75, TimeUnit.MILLISECONDS);
    if (cmd != null) {
        // ...
    } else self().send(new FetchLatest());
    // ...
}

In addition, Quasar Web Actors by default automatically assign a new actor to a new HTTP sesssion or WebSocket connection, so the callback-based application controller in the Typesafe version is not needed at all in Quasar, where everything is directly handled by the actor, which sees the browser (or mobile client) simply as another actor which it can watch to monitor for client termination.

Typesafe’s Tutorial about the app mentions several types of design patters:

  • Reactive Push basically means allocating threads to actors efficiently to handle WebSocket exchanges. This is accomplished just as efficiently by using Quasar’s fiber-based actors and without restricting the usage of normal control flow constructs.
  • Reactive Requests and Reactive Composition basically mean using monadic composition of async constructs like Futures in order to achieve efficient threads usage in web services. This complexity is completely unnecessary when running on fibers: you can use regular, straightforward blocking calls and control flow, and the fiber scheduler handles threads for you to achieve the same effect and performance.
  • The Reactive UIs have basically just been copied over to Quasar Stocks.

Finally, Web Actors are 100% Servlet compatible so there’s no need to run a non-standard embedded server if you don’t want to. In contrast, Play has to run standalone 2.

Performance comparison

The ring-bench JMH benchmark suite, based on and forked from fiber-test, compares several message passing implementations based on Akka, Quasar Actors, Java Threads and Quasar fibers with or without channels of different types.

The benchmark arranges worker actors in a ring and performs a message passing loop. The variables are:

  • The number of worker actors (default = 503)
  • The length of the loop (default = 1E+06 message exchanges)
  • The number of rings (default = 1)
  • The business logic performed before each message exchange and its parameters (default = none).

All the tests have been performed on a MacBook Pro aluminium late 2008, 8GB RAM, Core 2 Duo P8600 2.4Ghz under Mint Linux (Linux 3.13.0-49-generic), with JDK 1.8.0_45-b14 with aggressive optimisations and tiered compilation enabled. The JMH version used was 1.8 with 5 forks, 5 warmup iterations and 10 iterations.

Let’s first have a look at the memory footprint with default parameters:

Memory footprint

Compared to both fibers and Quasar actors, Akka has the highest heap usage, the highest number of GC events and the highest total GC time, so Quasar has overall a lower memory footprint.

As for speed, the first thing to note is that varying the number of worker actors, even up to millions, doesn’t change single-ring performance figures for Quasar nor for Akka: this confirms that actors (and fibers) are indeed very lightweight.

Then two sets of measurements have been made: the first one, with a fixed number of 1E+04 message exchanges and varying business workload, shows that Quasar starts slightly faster but as the workload starts dominating, Akka and Quasar start performing very similarly:

Increasing load chart

With no workload and a varying number of message exchanges instead, we measure pure framework overhead. Again Quasar starts faster but then Akka takes the lead and the additional overhead of Quasar reaches and stabilizes at around 80% more than Akka:

Increasing load chart

A JMH perfasm profiling round highlights the additional cost of the real lightweight threads in Quasar related to the user stack management, due to missing native continuations in the JVM. Akka doesn’t offer real lightweight threads, so it doesn’t have that overhead.

Of course, any overhead – no matter how small – is comparatively much bigger than no overhead. To understand whether the overhead is significant in practice we must compare it to actual workloads. With a per-message business workload equivalent to sorting an int array of 224 elements or, equivalently, a pre-compiled 6-length digits-only regexp (failing) match on a 1700 bytes alpha-only text (3 to 4 microseconds on the benchmark system), Quasar is less than 1% slower than Akka.

Practical performance

This means that in the worst case, for an application that on average does at least the equivalent of a 6-char regexp match on a 1700 bytes text per message exchange, the performance difference will be less 1%. Since most applications do much more than that, in practice you can get the extra programming power Quasar fibers and actors can offer with the same performance of Akka 3.

Conclusion

Quasar is a fast, lean and pragmatic concurrency library for Java, Clojure and Kotlin offering real lightweight threads and many proven concurrency paradigms, including an implementation of the actor model practically identical to Erlang’s. Quasar also has low integration, adoption, and opt-out costs. Its functionality is focused, and where it does provide extra features such as logging and monitoring, it uses standard APIs (SLF4J, JMX etc.).

Akka is an application framework and – like other Typesafe frameworks like Play – is a totalizing choice, encompassing the entire application, introducing its own rich, APIs (even for logging), testing harness, monitoring and deployment.

Akka – even its Java API – is very much influenced by Scala, and may feel foreign to Java developers. Quasar actors feel very familiar and idiomatic whether you’re writing Java, Clojure or Kotlin.

Akka’s API is callback-based. Quasar provides true fibers – like Erlang or Go – so blocking is free and Quasar’s actor API is simpler, more familiar and more compatible with other code. Being blocking and fiber-based allows the use of a very small number of basic concepts – just like in Erlang – whereas Akka introduces many unfamiliar and redundant concepts. For example, to work around the lack of a simple blocking selective receive (offered by Erlang and Quasar), Akka must introduce message stashing. Other concepts (like monadic futures) have nothing to do with business logic or the actor model, but are pure accidental complexity.

Akka is certainly the way to go if:

  • You have embraced Scala and like its programming style.
  • You are not afraid of betting on a framework and sticking with it, nor of paying a potentially high redesign / rewrite price for opting out.
  • You are prepared to pay for production monitoring, or willing to code your own monitoring solution.

Otherwise I suggest you give Quasar a try: it is production-ready, lightweight, fast, standards-based, easily integrable, completely free and open-source, and offers more than Akka’s asynchronous actors for less complexity.


  1. There are several actor systems that do not support selective receive, but Erlang does. The talk Death by Accidental Complexity, by Ulf Wiger, shows how using selective receive avoids implementing a full, complicated and error-prone transition matrix. In a different talk, Wiger compared non-selective (FIFO) receive to a tetris game where you must fit each piece into the puzzle as it comes, while selective receive turns the problem into a jigsaw puzzle, where you can look for a piece that you know will fit.

  2. Unless you use a 3rd-party plugin with some limitations.

  3. Above 2048 bytes Quasar becomes faster than Akka but the reasons are so far unclear, it may related to more favorable inlining.

Join our mailing list

Sign up to receive news and updates.

Tags:

comments powered by Disqus