Structured Concurrency at Work in an ObjectOriented Language Jürg Gutknecht 1, Brian Kirk 2, Roman Mitin1 1
Institute for Computer Systems, Clausiusstrasse 59, 8092 Zurich, Switzerland {gutknecht, mitin}@inf.ethz.ch
2 Robinson Associates, Weavers House, Friday Street, GL6 6QJ Painswick, Gloucestershire, United Kingdom
[email protected]
Abstract. Now that processor speeds have reached a physical limit (approx 3 GHz) there is a strong trend to using computing platforms with multiple processors (cores) to extend performance. At the same time significant applications which are intrinsically concurrent on a variety of levels need to be programmed in a clear and structured way. The Zonnon language accommodates these demands by promoting a new computing model that combines structured concurrency with object-orientation. The model can be mapped directly and quite naturally onto platforms with a single or multiple processors sharing a single memory space, or to a network of processors each with its own memory space.
1 Introduction With a new computer language one not only learns a new vocabulary and grammar but opens oneself to a new world of thought … –
Niklaus Wirth
Zonnon is a new programming language in the Pascal, Modula-2 and Oberon family. It is designed for writing well structured programs in a variety of programming styles including procedural, object-oriented and concurrent; retaining an emphasis on simplicity, clear syntax and separation of concerns. Although more compact than its peers C#, Java etc., it is a general-purpose language equally suitable for ‘programming in the large’ as for ‘programming in the small’. Zonnon provides a rich object model that incorporates both the behavior of objects and their interoperation in terms of a single dynamic hierarchy of concurrent activities subsuming abstractions of three basic code patterns: a.) critical sections run under mutual exclusion, b.) sets of independent statements delimited by barriers, and c.) asynchronous procedure calls.
2 Introducing activities Activities are used in Zonnon for adding behavior to objects and for implementing communication. They integrate concurrency seamlessly into the language and provide the element needed for creating ‘structured concurrent programs’. Activities can be declared wherever procedures can be declared and they resemble them syntactically. However the difference lies in the runtime model. When a procedure is called, possibly with some parameters, it then runs on the same execution thread as its caller. On completion it returns execution to the caller, along with any return parameters. In contrast, an activity is instantiated, possibly with some initial parameters that characterize it, and then it has an execution thread separate from its creator. An activity may optionally implement a communication protocol which syntactically defines an exchange of tokens with its creator. An arbitrary number of ‘child’ activities may be created from within the ‘parent’ scope. Child activities may be statically nested. They are instantiated by any parent within its scope of visibility by using the new operator followed by the name of the activity and a matching list of actual parameters. If the ‘parent’ scope is marked with a leading {barrier} modifier, then its end by definition takes the role of an execution ‘barrier’ that must not be crossed until all ‘child activities’ have terminated. 2.1 Active objects Simple concurrent systems can be built using just “active” modules. One or more activities are declared in the modules and then instantiated during module initialization: module Writer; activity A; (* declare activity *) begin writeln("A") end A; begin new A; (* run the activity *) writeln("B") end Writer.
Note that the order in which the symbols "A" and "B" are displayed is nondeterministic. Similarly, activities can be declared within objects instead of modules. In its most general form, an object (or module) body becomes active by acting as the root of an entire hierarchy of encapsulated activity. For example the well-known Quicksort algorithm is an activity that splits into locally independent sub-activities in a natural way. Roughly, the algorithm works like this: Partition the array to be sorted; Sort left part; Sort right part
Clearly, the latter two actions are mutually independent and can therefore run concurrently in principle. Making use of child activities, the potential parallelism can easily be expressed in Zonnon. In fact, if var a: array 1 + N of integer; (* a[1] … a[n] used *)
is the array to be sorted, a parallel version of Quicksort in Zonnon reads like this: activity Sort (L, R: integer); var i, j, t, x: integer; begin {barrier} i := L; j := R; x := a[(L + R) div 2]; repeat while a[i] < x do i := i + 1 end; while x < a[j] do j := j - 1 end; if i j; if L < j then new Sort(L, j) end; if i < R then new Sort(i, R) end end Sort;
Note that two new instances of the Sort activity are created recursively after the partitioning phase of each running instance of the Sort activity. Their task consists of sorting one of the two parts (left or right). Obviously, in any real sorting scenario, a large number of short-living activities are generated by the above recursion. In view of the typically limited number of processors available, it is hardly advisable to create a new thread for each activity. A much better strategy is using recycled threads from some existing runtime thread pool. The entire sorting machinery is launched by simply calling new Sort (1, N)
2.2 Shared objects Zonnon objects can be made sharable by marking their type with a {protected} modifier, in which case execution within them is limited to only a single activity and thus they essentially become a Hoare monitor [1], however in a refined version that supports shared locks. Individual methods of a protected object type can be declared as {shared}, providing sharing of the object’s lock and read access, but not write access, to the state within the protected object. Example: object {protected} SharedObject; var value, hidden: integer; procedure {public} Exchange; var t:integer; begin value, hidden := hidden, value end Exchange; procedure {public, shared} Get: integer; begin return value; end Get; end SharedObject.
The concept of shared objects was originated in [2] and developed and proven in [3]. Only a single scheduling primitive is required for programming concurrency in
such systems – the await statement, proposed by Per Brinch Hansen for Concurrent Pascal [4]. In Zonnon this statement takes the form await cond;
cond is an object-local Boolean expression that defines the precondition for the continuation of execution. In a sense await serves as a guard with a semantics similar to the guards in Dijkstra’s guarded commands [5]. 2.3 Dialogs The model of hierarchically created local activities as explained above crucially relies on the availability of shared memory. This is an unrealistic assumption in the case of object interoperation because different objects may well be allocated in disjoint address spaces. It is therefore advisable to amalgamate the model of communication between parent activity and child activity by some form of message passing. The form used in Zonnon is protocol-based dialogs, where a protocol defines the valid sequences of tokens exchanged between the corresponding pair of activities in a syntax expressed in EBNF [6]. In such a case the child activity takes the role of an “agent” that interoperates with the parent via the predefined protocol. Parent activities use a procedure-call like notation for expressing the passing of tokens. A simple form of ‘multiple concurrent assignment’ [5] allows to conveniently receive a list of tokens. This notation is justified because a conventional (“synchronous”) procedure call can be considered an equivalent of a simple dialog: create an activity of the corresponding procedure type; send the list of actual parameters; receive the result
Note that an “asynchronous” procedure call can equally easily be modeled as a dialog because the send operation is non-blocking and an any action may be run between sending the last actual parameter and receiving the result. We now use John Trono’s Santa Claus programming exercise [7] to illustrate the power of dialogs: “Santa Claus sleeps at the North pole until awakened by either all of the nine reindeer, or by a group of three out of ten elves. He performs one of two indivisible actions: If awakened by the group of reindeer, Santa harnesses them to a sleigh, delivers toys, and finally unharnesses the reindeer which then go on vacation. If awakened by a group of elves, Santa shows them into his office, consults with them on toy R&D, and finally shows them out so they can return to work constructing toys”. We slightly extend this exercise by adding an option for elves to withdraw their application in the case that waiting would take overly long. Here is the corresponding protocol in EBNF format, where “?” symbolizes messages from child to parent, and where join, wait, and done are keyword tokens: ElfProtocol = join [ ?wait (join | done) ] ?done.
From the perspective of Santa’s manager (the child activity) the implementation of the dialog looks as follows. Formal parameters and the accept statement is used for receiving tokens (from the parent), while return is used to send tokens (back to the parent). The ElfDialog activity is supposed to implement the ElfMsg protocol: protocol ElfMsg = (join, wait, done, ElfProtocol = join [ ?wait (join | done) ] ?done ); module Manager;
activity ElfDialog(req: ElfMsg) implements ElfMsg; begin if (*it takes too long*) then return ElfMsg.wait; accept req end; if req = ElfMsg.join then (*take care of elf*); return ElfMsg.done end end ElfDialog; end Manager.
The parent’s part of the same dialog is implemented as an intrinsic activity of the Elf object type. Note that the dialog is “stateful” as the varying semantics of arriving ElfMsg.join tokens shows: type Elf = object activity Work; var res: ElfMsg; d: Manager.ElfDialog; begin loop … (*construct toys*) … d := new Manager.ElfDialog(ElfMsg.join); res := d; if res = ElfMsg.wait then if (*impatient*) then d(ElfMsg.done) (*withdraw*) else res := d(ElfMsg.join) end end end end Work; begin new Work() end Elf;
3 Using agent activities for structuring 1
2
6
3
5
4
8
7
Fig. 1. Objects with various elemental structures of concurrency. In a sense dialogs are “active links”. Consider, for example, the situation in Figure 1. The (active) Object 1 runs four encapsulated activities. Object 2 is similar to Object 1 but has spawned an agent activity in Object 3. Object 4 maintains an entire tree of “active links” to Objects 5, 7 and 8. This constellation is turned into an entire network of links by the parent activity in Object 7 that creates a child (agent) activity in Object 8. Objects 4, 5 and 6 illustrate an (active) chain of interoperating objects.
4 Conclusions Structuring concurrency is analogous to structuring data. Many programmers today write multithreaded programs that work (in most circumstances). However, current programming languages and techniques yield understandable concurrent programs only for very simple interactions [8], and a step comparable to the introduction of Pascal towards structure is urgently needed. The Zonnon language is an attempt in this direction. It is a conceptual evolution of Active C# [9] and provides a much better integration of concurrency than previous techniques [10] that heavily rely on “thread libraries” and on operating system features. A working Zonnon compiler for .NET is available on www.zonnon.ethz.ch. Future efforts will be necessary to optimize the mapping of the concurrency constructs to .NET and to enhance the compile-time checks for dialogs.
5 Acknowledgment The authors gratefully acknowledge the work of their former project collaborators Eugene Zueff and Urs Müller. They would also like to thank Svend Knudsen for numerous fruitful discussions in connection with sets of independent statements.
References 1.
C. A. R. Hoare. Monitors: an operating system structuring concept, Comm. of the ACM, 17 (1974), pages 549–557 2. J. Gutknecht. Do the Fish Really Need Remote Control. A Proposal for Self-Active Objects in Oberon. JMLC 1997: 207-220 3. P. Muller. An Active Object System Design and Multiprocessor Implementation, PhD Thesis, ETH Zurich 4. P. Brinch Hansen. The Architecture of Concurrent Programs, 1977 5. E.W. Dijkstra. Guarded Commands, Non-Determinacy and a Calculus for the Derivation of Programs, EWD418, Jun 1974 6. ISO/IEC 14977 Extended Backus-Naur Form, 1996 7. J. A. Trono. A New Exercise in Concurrency. SIGCSE Bulletin, 26(3):8-10, 1994 8. Edward A. Lee. The problem with Threads, in IEEE Computer, 39(5):33-42, May 2006 9. R. Güntensperger, J. Gutknecht. Active C#. .NET Technologies 2004, Workshop Proceedings: 49-56, V. Skala, P. Nienaltowski (Eds.), 2004, University of West Bohemia, Plzen, Czech 10. B. Kirk. Designing Systems with Objects, Processes and Modules, Proceedings of BCS Conference on Software Engineering 90, Brighton, England, July 1990