2020/01/25

Coming from Java, Part III (Singletons)


(This entry is the third installment of "Coming from Java". This is Part III; here is a link to Part II.)

There is a common pattern in Java, which is the singleton pattern. Basically, it allows you to create a single instance of a class, and then to be able to find that one same instance from anywhere in your code
public class Singleton
    {
    public static final Singleton INSTANCE = new Singleton();

    private Singleton()
        {
        // initialization stuff goes here
        }
    }
Creating a singleton in Ecstasy is accomplished by using the static keyword for the class:
static const Singleton
    {
    construct()
        {
        // initialization stuff goes here
        }
    }
There is one tiny detail, though: In Ecstasy, only const classes and service classes can be singletons, and the reason is quite fundamental to the purposes for which Ecstasy was designed.

To begin with, consider the differences between high-end computers that Java was designed for, and the high-end computers that Ecstasy is designed for. At the time that Java was initially being designed in the early 1990s, very few computers had more than one CPU -- Sun hadn't yet even released its first multiprocessor workstation! A server or a high-end workstation might have had 8MB of RAM, and -- still hard to believe! -- the IBM 370 mainframe of the day topped out at 16MB of RAM. That's megabytes -- a new notebook today has a thousand times that much memory!

Over a decade later, with dual-CPU servers now the norm, Java would get its first working memory model specification, specifying the JVM's guarantees for reads and writes occurring across multiple CPUs and among multiple threads.

Ecstasy, in contrast, was designed explicitly to take advantage of computers with potentially many thousands of cores, and with potentially many terabytes of main memory. To accomplish this, the design focused on disentangling threads from each other, and disentangling the memory -- what Java calls "the heap". The rationale is simple: In a modern computer, a single thread of code can perform on the order of 1-10 billion instructions per second, if and only if the thread does not share read/write memory with any other threads. The moment that a thread starts to use read/write memory that is being used by other threads, the performance (and the predictability of the performance) drops like a lead balloon.

To avoid the lead balloon effect, Ecstasy carves out exclusive zones of mutable memory, each with its own single conceptual thread. Each of these is called a service. An Ecstasy service can be thought of as a simple Turing Machine, or a simple von Neumann machine. And an Ecstasy service can be thought of as a boundary for mutability, because all mutation of a service's memory occurs within that service, and only immutable data can permeate that boundary. Services can communicate with other services, but that communication is conceptually asynchronous in nature, the communication is in the form of invocation, and only immutable data is exchanged.

Since a singleton is, by its nature, visible to all code running in an application, it therefore stands to reason that the singleton must be immutable -- so that it can be used by code running in any service -- or the singleton must itself be a service -- so that it can be invoked by code running in any other service.

And here is a straight-forward example in Ecstasy:
static service PageCounter
    {
    Int count = 0;
    Int hit()
        {
        return ++count;
        }
    }
Using the singleton is equally simple:
PageCounter.hit();
(Since it is a singleton, the name of the class implies the singleton instance.)

The same example can be constructed in Java, but thread safety is the responsibility of the programmer:
public class PageCounter
    {
    public static final PageCounter INSTANCE = new PageCounter();

    private PageCounter() {}
    
    private int count;
    
    synchronized public void setCount(int count)
        {
        this.count = count;
        }
    
    synchronized public int getCount()
        {
        return count;
        }
    
    synchronized public int hit()
        {
        return ++count;
        }
    }

// how to call the singleton
PageCounter.INSTANCE.hit();
There are a variety of ways to implement the counter in Java in order to make it more concurrent; for example, an atomic integer class can be used, or an atomic updater on a volatile field can be used, and so on. In Ecstasy, on the other hand, the choice of how to make the counter more concurrent is left completely up to the run-time implementation. The choice to allow the run-time to optimize this facet of execution is based on what we learned from Java's own HotSpot JVM -- which is that only the run-time has enough information to know which parts of the application would actually benefit from optimization in the first place, and which optimizations would work best, based on the actual run-time profiling information!

A few miscellaneous notes to wrap up this singular topic:
  • In Ecstasy, every module, package, and enum is a "static const" class, automatically. That means that modules and packages are all singleton objects, and every enum value is a singleton object.
  • Ecstasy does not require a memory model for explaining the order of reads and writes of mutable data among threads, because (as explained above) the Ecstasy design does not have mutable shared state among threads. (The Ecstasy design also uses services in lieu of explicit developer-managed threads, but that is a topic for another blog entry.)
  • There is no "global heap" in Ecstasy, so there is no "stop the world" garbage collection. Ecstasy is automatically garbage-collected, but each service can manage its own memory. The Ecstasy design effectively eliminates the "GC pause" problem, even for programs that use terabytes of RAM.

 

No comments:

Post a Comment

All comments are subject to the Ecstasy code of conduct. To reduce spam, comments on old posts are queued for review before being published.