Discussions on Join Concurrency
-
Chords(or join pattern) are the core of concurrent designs based
on Join:
Traditional multi-threaded systems
focus on managing threads and their synchronization. However
there are many issues with using threads directly as program
structuring tools, especially on today's multi-core machines:
- Threads are still expensive resources on most systems.
- It is difficult to partition application functionalties directly
into correct number of threads: Undersubscription
happens when there are not enough running threads to keep
CPUs busy. Oversubscription happens
when there are more running threads than physical CPUs, which results
in time-sliced scheduling of threads with the overhead of context
switching, cache cooling etc.
- Scalability: because of the above factors, creating more threads
doesnt guarantee better performance, up to certain point, more threads
can even slow applications.
Newer concurrency systems are switching
to task based designs, such as Java's executor framework and Intel's
Threading Building Blocks library:
- a task is a much lighter weight (compare to thread) logical unit
of work; partition application functionalities into tasks
- manage threads centrally (as pools); for CPU bound applications,
typically spawn a thread per CPU.
- mapping / dispatching tasks to threads in pool to execute;
amortize the cost of threads management across all tasks
- as long as application work is broken into small enough tasks,
load balancing is automatically achieved in thread pool
In Join based applications (Cω and Join), chords play the core
role of concurrency design:
- chords define synchronization:
synchronization only apply to
async<> / synch<> ports which participate in the chords
of same joint.
- chords define concurrency:
In
Join based systems (Join or Cω)
there is no explicit thread creation and synchronization or even no
explicit
task creation and dispatching to executor thread pool. Chords
with only
async<> ports will implicitly (automatically) create a task for
its body and
dispatch it to the thread pool of the executor
associated with the joint. All
concurrency and
asynchroncy are defined and created by async<> ports and chords.
- Programming model : orchestration of asynchronous activities:
- new ways to maintain object state and its integrity
In normal concurrent applications,
member variables are used to represent the state of objects and locks
are used to coordinate the shared access by multiple threads. In Join
based design, async<> ports are used to represent the states of
active objects, and chords with these "state-describing" async<>
ports
can define what behaviour (synch<> / async<> ports) are
valid in different object states. Partial specialization of
async<> template class reduce the overhead of async<> ports
to minimum.
- new usage of mutual exclusion
The synchronization design of Java and
C# is based on monitor, where a object-wide lock is used to guarantee
that only one of "synchronized" methods can be called at anytime and
normally the lock is hold during the execution of the method body (a
relatively long period).
In Join based systems, when a async<> / synch<> call or
message arrives, a object-wide lock is only used to check if any chord
is ready to fire (a really short while). More precisely, deciding
whether any chord is enabled by a call and, if so, removing the other
pending calls from the queues and scheduling the body for
execution is an atomic
operation. Apart
from this atomicity guarantee, however, there is no monitor-like mutual
exclusion between chord bodies. So when a chord
does fire and
during execution of chord body, there is no lock hold anymore. Any
mutual exclusion that is required must be programmed explicitly in
terms of synchronization conditions in chord
headers.
In shared-state model, control flow
thru objects: objects are passive, calling
thread invoke objects' methods and execute the method body.
In Join based model, active objects are active in that they are
responsible for maintaining the integrity of their own state. With
async<> ports defining interface, control flow stops at object
boundary/interface and data flow thru objects. Using async<>
ports to represent object states, chords (join pattern) of these
"state-describing" async<> ports and other
async<> / synch<> ports can define what behaviour
(synch<> / async<> ports) are
valid in different object states.
In shared-state model, the interface of
concurrent behaviour is still normal objects methods (functions);
synchronization among concurrent object behaviours (methods) is
indirectly programmed using
locks. In Join based model, the interface
of concurrent behaviour consist of
async<> / synch<> ports, and mostly async<> ports
for a loosely coupled interface; and synchronization of concurrent
object behaviours (methods) must be explicitly
specified as join
pattern in chord headers. Normal object methods are mostly used for
internal implementation or non concurrency interface.
- Inheritance based composition
In child class of "active" classes
(classes with joint as base), the following extensions can be performed:
- defining new async<> / synch<> ports
- defining new chords with new or exisitng
async<> / synch<> ports from different parent classes
- interface / implementation, "abstract" active classes
Separating interface and implementation
is a basic principle of OO design. In C++, interfaces are normally
defined as abstract classes with pure virtual methods which have only
headers/signatures and no body defined.
In Join, an async<> / synch<> port is "pure
virtual" (has no body) before chord() is called which associates it
with a body. So in Join based OO concurrent designs, interfaces can be
defined as "abstract" joint based classes which only define
async<> / synch<> ports while the definitions
of
chords and bodies are deferred to children / implementation classes.
There is a difference in error handling: C++ compilers will prevent
instantiations of abstract classes, and because Join is
implemented as a library, compilers cannot prevent instantiation of
"abstract active" classes, the only help are runtime "not_in_chord"
exceptions which are thrown when such objects are used.
Please refer to "Active
Objects Tutorial" and "
Asynchronous
Call and Return Patterns Tutorial" for examples of interface and
extension.
In normal C++, a method can be declared
"virtual" and can be overriden in child classes (the same header /
signature are re-defined with a new body).
In Join, for async / synch ports, their bodies are chords which
may be defined with multiple method headers. So in Join, we are talking
about chord-overriding and when a chord is overriden, all its method
headers are "overriden" in the sense of that they all exhibit new
behaviour.
Join supports two kinds of overriding. The chords defined can be
overriden
with new bodies either thru "virtual" chord body methods or by calling
chord_override():
When we define async/synch ports and
their chords, the chord "body" method can be declared "virtual". Then
in child classes, this chord body can be overriden just as any other
virtual methods. When an instance of this child class is used anywhere
thru pointers or references to parent class (by calling its async/synch
ports), the proper child class chord body method definition will be
invoked.
This refers to the capability that
chord definitions can be changed by overriding or replacing chord body
methods during runtime when code runs. For this purpose,
chord_override() is called with the set of async / synch ports (same
as the chord to be override) and the new chord body method. The
identified chord will replace its chord body with the new method.
Please refer to "Chord Overriding Tutorial" for examples of both
static and dynamic overriding.
- Aggregation/delegation based composition
Aggregation and delegation promote a
kind of software reuse design similar to circuit integration: creating
large/outer/containing components (joints) by composing a network of
smaller/inner components (joints). Outer/containing components / joints
are defined thru the following ways:
- "composing": create and manage the life time of inner components
(joints) and set up the interactions among them.
- "aliasing": expose interfaces of inner joints (their
async<> / synch<> ports) selectively
and directly to
outside at outer/containing joints by C++ references (or pointers) to
async<> / synch<> ports of inner joints.
- "adapting": new interfaces (async<> / synch<> ports)
can be defined at outer/containing joints which are
implemented by invoking inner joints' methods.
Please refer to "Extending by Aggregation and Delegation Tutorial" for
a sample.