DataKernel’s primary mission is to create ultimately fast, scalable, simple to use, high-abstraction level I/O async
To achieve this, DataKernel’s design principles overcome all performance overhead and complexities of the traditional
multithreaded programming model yet fully utilize Java multithreading capabilities.
DataKernel offers means of splitting the application into a Primary Eventloop thread and Worker
Eventloop threads. These threads communicate with each other via message passing and thread-safe application-specific
Each Eventloop thread is essentially a single-threaded mini-application (similar to Node.js), which handles its part
of I/O tasks and executes Runnables submitted from other threads. Primary Eventloop threads distribute and balance
I/O tasks between Worker threads.
The benefits of DataKernel threading model:
Each primary/worker Eventloop thread works as a single-threaded application, which is simple to program and to reason about
There is no multithreaded overhead, races, and thread synchronization overhead
Traditional strength of Java in multithreaded programming is fully preserved:
typical I/O load can be easily split between worker threads
the application can have thread-safe singleton services, which are used by Eventloop threads, and a huge singleton
data state, shared among all worker threads
you can still use some thread synchronization / lock-free algorithms, just try to avoid excessive blocking of
full interoperability between Java Threads, Thread Pools, Java Futures and even blocking I/O operations
However, this design also raises some implementation questions.
For example, if we want to implement multithreaded HTTP web application with worker eventloops:
according to these design principles, we need to create separate instances of a working eventloop, a single-threaded HTTP
server and its servlets for each working thread
but what if our application has tens or hundreds of such single-threaded components that belong to their own worker
for example, if we have 8 eventloop threads with 10 worker-thread components inside, do we have to create 80 of
components in total and assign them to each worker thread?
how is it even possible to do it manually: to instantiate, wire, initialize, start/stop all those components (both
singletons and worker objects) in a correct order; to gracefully shutdown application on start/stop errors?
Luckily, due to DataKernel DI, we have a solution - @Worker scope. So, if you need to implement several worker threads:
include WorkerPoolModule module and create a WorkerPool instance
annotate the components you wish to put into each worker thread with @Worker scope annotation
and WorkerPool will automatically instantiate identical dependency graphs for each of those worker threads
you are by no means limited to aforementioned scheme with one primary Eventloop and N worker eventloops:
you can still have completely unrelated / standalone eventloops (nor primary, neither worker)
or several primary eventloops, sharing the same pool of worker eventloops
or several sets of worker pools with different number or threads
you can even define your own @Worker annotations, and create multiple worker pools with completely unrelated and
different dependency graphs
all this is in fully transparent and easy-to-understand modules - just mark different components with appropriate
worker annotations and let WorkerPool to create all the instances
to automatically start/stop application components in correct order, simply include ServiceGraph module into your
Launcher - it is aware of worker pools and will treat vectors of worker instances as special compound singleton-like
For example, here is a Multithreaded HTTP Server:
And its dependency graph looks as follows:
To help you understand how worker pools work, here is a simplified WorkerPool implementation in a nutshell (the actual implementation differs, but not much):
As you can see, the root Injector simply ‘enters’ the worker scope N times, so we have N Injectors with identical
bindings/dependency graphs, but different containers of instances. Each time we need to create some worker instances,
they are created N times by each injector and returned as a vector of N instances.
To run the examples, you need to clone DataKernel from GitHub:
$ git clone https://github.com/softindex/datakernel And import it as a Maven project. Check out branch v3.1. Before running the examples, build the project.
These examples are located at datakernel -> examples -> core -> boot
Basic Worker Pool Example
An example of creating a worker pool with 4 workers:
The dependency graph of the example includes the created worker pool and looks as follows:
Multithreaded Worker Pools Collaboration
Several Worker Pools can co-work to calculate a single task. In this example we have 25 Workers and each of them has its
own Eventloop. These Eventloops are wrapped in Threads and then added to the list of threads. After that the
list is permuted and the threads with Eventloop tasks start. The task is to put Eventloop id in the ConcurrentLinkedQueue
in accordance to the delay (the id multiplied by 100). In this way we receive an ordered queue of Eventloop ids, after that
the Threads park and the queue is emptied.