C. A. Cunha, J. L. Sobral2
1
Departamento de Informática, Instituto Politécnico de Viseu, Portugal 2
Departamento de Informática, Universidade do Minho, Braga, Portugal
1
Abstract
This paper presents a programming language for parallel computing based on code annotations. It has similar goals and philosophy as OpenMP but it is more tightly coupled to the object oriented paradigm. We include annotations for most common concurrency patterns and mechanisms, namely, one-way, futures, barriers, reads/writers and thread-local. Our current prototype is implemented using Java 5 annotations and AspectJ and provides a feasible and efficient alternative to the Java thread model.
1. Introduction
Multi-core and multiprocessor machines are becoming mainstream architectures. However, to take advantage of this type of architectures, applications should go through several modifications, as to specify tasks that can run in parallel and to perform synchronisation among tasks. This incurs in an additional burden to the programmer and may involve non-reversible changes to applications.
Traditional thread programming can be used to convert a sequential application to a parallel counterpart, however it lacks of suitable abstractions to help the programmer to structure parallel applications and may require a considerable amount of code rewrite to fit into the thread parallelisation model.
OpenMP [1] provides a more structured way to introduce parallelism into a sequential application. It includes parallel blocks and parallel loops (e.g. for), as well as a set of synchronisation directives for shared variables. Parallelism related concerns in OpenMP are specified through a set of directives that can be ignored by a non-compliant compiler, achieving a valid sequential program. This approach makes parallel code closer to sequential versions, softening the transition from sequential to parallel programming. OpenMP is * This work was supported by PPC-VM project (Portable Parallel Computing Based on Virtual Machines, POSI/CHS/47158/2002) and by SeARCH (Services & Advanced Computing with HTC/HPC, CONC-REEQ/443/EEI/2005) both funded by Portuguese FCT (POSI) and European funds (FEDER).
currently supported by important compilers, namely Intel, Microsoft and GNU compilers.
There is still no official binding of OpenMP for Java and, most important, OpenMP is targeted for traditional structured programming and not for object-oriented programming models. As consequence, it can not take advantage of the structure of object oriented applications (e.g., classes and method calls). In this article we propose a framework comprising a set of annotations intended to replace OpenMP directives in the context of object oriented applications. In this approach we annotate classes, methods and instance variables instead of generic blocks of code, resulting in a more high level and object-based reasoning. The provided annotations include: @Oneway, which spawns a method call in a new thread; @Barrier, which blocks a set of threads until all have reached a certain point in program execution and @ThreadLocal, which creates a local instance variable per thread.
The framework was implemented in Java using Java 5 annotations [2] and AspectJ [3]. This approach provides a fully integration into the Java language, including compile time check of annotation syntax and direct generation of Java bytecodes, without intermediate compilation steps. This avoids a common problem, in pre-processor based approaches, of tracing an error to the correct point in source code.
This work differs from other research works in the way it uses programmer based annotations to specify parallel execution and synchronisation, in a philosophy similar to OpenMP. It differs substantially from OpenMP, since annotations are provided at instance variable/method level and are fully integrated in an object oriented model.
The remainder of this paper is organised as follows. Section 2 presents related work. Section 3 describes the proposed annotations from a programmer perspective. Section 4 overviews the current implementation of these annotations by means of Java 5 annotations and AspectJ. Section 5 shows several code examples and section 6 presents performance evaluation. The last section discusses obtained results and future work.
2. Related Work
Early work on objects and concurrency introduced several high level patterns and mechanisms to address the complexity of concurrent programs. One of these early efforts was concurrency annotations [4]. In this approach Eiffel programs are annotated to introduce a parallel semantic, i.e., an alterative semantic that supports parallel execution. Provided annotations include compatibility annotations (a generalisation of readers and writers methods), delayed acceptance based on Eiffel preconditions, autonomous objects (i.e., active) and asynchronous invocations. This model was recently ported to Java [5].
Another relevant effort was COOL [6], an extension to C++ that provides parallel methods that run on a separate thread and event synchronisation, a mechanism similar to Java monitors. Although this model has a philosophy similar to concurrency annotations it introduces new keywords to C++ to provide a parallel semantic.
ProActive [7] includes active objects and automatic future method calls (a mechanism called wait-by-necessity). ProActive does not rely on programmer annotation to express parallelisation issues, as it is based on a more implicit parallelisation model.
A large amount of Java based approaches are concerned to Java distributed machines aimed to address distributed thread execution, supporting a single system image. Examples are cJVM [8], Hyperion [9], Jakal [10] and JESSICA2 [11]. These approaches rely on the Java thread model to introduce concurrency into sequential applications.
An OpenMP bind for Java is presented in [12]. Directives are introduced through special Java comments. An external tool converts an annotated program standard to Java code. In [13] a similar bind is presented, better fitted into Java language philosophy and [14] presents an implementation on distributed memory systems by means of a DSM middleware.
Our proposal differs from these previous efforts as we provide a richer set of annotations, fully integrated into an object oriented model. Additionally, we provide a complete implementation, based on Java 5 annotations, built as an AspectJ library§.
3. Annotations for Concurrent Execution
Annotations are a metadata facility introduced in J2SE 5.0 (Tiger) [15] implemented as modifiers that can be added to the code, namely to classes, interfaces, §
The AspectJ library can be downloaded from the PPC-VM web page at http://gec.di.uminho.pt/ppc-vm
methods and fields. Java Tiger includes built-in annotations and supports the creation of new custom annotations. Annotations describe the elements behaviour which contributes to a better code comprehension about the program behaviour.
Several annotations were implemented to describe concurrent behaviour. Such annotations are considered in the context of many widely known high-level constructs for concurrency [16][17], namely One-way, Future, Active Object, Barrier, Synchronised, Readers-Writer, Scheduler and Thread local. Each construct comprises one or more annotations, which can be used to describe specific element types, e.g. fields, methods or classes (Figure 1).
@ClassAnnotation
public class //... @MethodAnnotation public void execute(){} } Figure 1. Annotate elements Occurrences of annotated elements are intercepted by aspects to add the corresponding concurrent behaviour at those points (see section 4). We classify annotations into concurrency generation annotations and synchronisation annotations. Most annotations can be applied to a specific thread group, supplied as an annotation parameter. Concurrency generation annotations can associate the spawned thread to a specific group, while synchronisation annotations can be restricted to a specific thread group. This feature allows multiple annotations to coexist in the same point in the code. Synchronisation annotations also feature an optional lock id name. This allows several locks to coexist for the same object or sharing of locks among objects. 3.1. One-way One-way mechanism [16] applies to methods that run on a thread of their own: the client never blocks, waiting for some result. One-way pertains only to asynchronous void method calls – when the method does return a value, a future (3.2) should be used. One-way usage follows the syntax: @Oneway(threadGroup=[thread group id], saveState=[Yes|No]) @Oneway annotate methods which should be executed in parallel. A new thread will be created to execute each annotated method, associated to thread group specified in threadGroup parameter. When 3.2. Futures omitted, the new thread is associated with the thread group of the current thread. If the thread reference is Futures [16] allow two-way asynchronous required latter – e.g. to perform a join or sleep invocations of methods that return a value to the client. operation – the saveState parameter should be specified Futures are join-based mechanisms based on data as True. objects that automatically block when clients try to use The traditional fork and join algorithms require, in their values before the corresponding computation is addition to the thread spawning mechanism, a way to complete. During execution of methods, the Future is a synchronise (i.e., join) the main thread and spawned placeholder for the value that has not yet computed. threads at some point in the application. Join In typical situations, Futures are used when a operations are allowed by using variable stores the result of a computation, which will @JoinBeforeExecution and @JoinAfterExecution annotations to force the current thread to wait for all threads created by it to terminate, before or after the annotated method execution. Similarly, @SleepBeforeExecution and @SleepAfterExecution forces the current thread to sleep during an amount of time specified by parameter time. Interruption of threads are also allowed using @Interrupt to interrupt all threads created by the current thread and @InterruptAll to interrupt all threads created in the annotation context. @OnewayExecutor is a less flexible One-way implementation, but more efficient for fine-grained, non-blocking methods as long as threads present in the thread pool are reused several times, reducing thread spawning overhead and avoiding the unrestrained creation of threads. It has the following syntax: @OnewayExecutor(executorId=”x”, tasksGroup=[thread group id], poolSize=[size], chunkSize=[size]) OnewayExecutor uses java.util.concurrent.Executor service to execute one way method calls, which is based on Thread Pools. @OnewayExecutor supports some additional parameters. Each Executor is identified by an executorId and the number of threads in the thread pool is specified by poolSize. If omitted, the number of threads is the same as the number of processors (e.g., cores). Tasks submitted to the executor service can belong to a specific group (tasksGroup parameter) and can by agglomerated to reduce the number of tasks submitted to the executor. chunkSize parameter specifies the number of one way invocations agglomerated per each submitted task. By default chunk size is 1. Additional mechanisms deal with incomplete chunks (e.g., time out mechanisms). @OnewayExecutor does not support the sleep functionality, but is supports join operations (in specific executors and/or tasksGroups) and a cancel annotation, similar to @Interrupt. be used later. Consider the following code: a = someobject.compute(); ...// other statements a.doSomething(); The compute method is executed by the new thread. Instead of blocking at the computation phase – i.e. during the execution of compute – the thread blocks when the variable is actually accessed – i.e. when the method doSomething is executed. Our annotation-based version of Future uses @Future annotation to annotate non-void methods invoked asynchronously and @FutureClient to annotate methods that use values returned in consequence of asynchronous method invocations. In addition, when the return type cannot be instantiated automatically (e.g., it requires specific parameters) the user should provide the method getFakeObject – which implements the code that instantiate the fake object. 3.3. Active Object Active object decouples the invocation of methods from their execution [17]. Each object runs into its own thread of control. Whenever a client object invokes a method from an active object, the thread associated with the active object carries out the execution. Active object is an abstraction that merges objects with concurrency. One of the early systems to use active objects was ABCL [18]. Several applications can benefit from the use of active objects. Client applications such as windowing systems and network browsers employ active objects to simplify concurrent, asynchronous network operations. Multi-threaded servers, producer/consumer and reader/writer applications are also well suited for the use of active objects. The creation of an active object involves the creation of several support structures, including a proxy object and a scheduler [17]. Our @ActiveObject annotation completely hides this complex structure behind a reusable aspect. 3.4. Barrier Barrier establishes a set of synchronisation points where threads should synchronise. Each thread reaching one of that points blocks waiting for the other threads of the group. Methods where threads synchronise can be annotated either with @BarrierBeforeExecution or @BarrierAfterExecution, depending if it blocks before or after method execution. Element-pair values in annotations allow definition of parameter values that can be used in this case to store the number of blocking threads and the target thread group name. Element-pair values, namely nThreads and threadGroup should be assigned with the number of threads and the target thread group where the barrier should apply. For instance, for five threads blocking after method execution associated to thread group calculus, the annotation is: @BarrierAfterExecution (nThreads = 5, threadGroup = \"calculus\") When the number of threads is not specified in barrier annotation the barrier waits for all spawned threads. 3.5. Synchronised Synchronisation is implemented in Java language using the synchronized modifier on method declaration or using the block construct synchronized(