Additional references: Author="Colin Atkinson and Trevor Moreton and Antonio Natali", Title="Ada for distributed systems", Publisher="Cambridge University Press", Address="Cambridge", Year=1988, Author="John C. Knight and John I. A. Urquhart", Title="On the Implementation and Use of Ada on Fault-Tolerant Distributed Systems", Journal="ACM Ada Letters", Volume="IV", Number=3, Month=Nov, Year=1984, Pages="53-64", Capsule summary: Ada is not a distributed programming language, it's not even a very good language for concurrency. WHAT IS IN ADA? Important issues: what would be the parts of a distributed program? does the language enforce communication via message passing? does the type system check communications? Syntactic units: An Ada program is a (main) procedure, and whatever subprograms and packages its uses from a library. Program units: tasks (parallel processes) subprograms (procedures and functions) packages (modules) generics All program units are library units, with the exception of tasks! Tasks must be nested in procedure or package. => tasks cannot be modules of a distributed program. => temptation to have globals. All program units nest. Packages and tasks are split into an interface decl ("specification") and a body, which can be compiled separately. Statements and blocks also nest (unlike Pascal, can have local blocks) Each program unit, block, record declaration, etc. forms a scope Syntax of task interface declaration: task [type] is IDENT [is {entry_decl} end [IDENT]]; -declarations that are not of types are equivalent to declaration of anonomyous type followed by a variable declaration of that type. -tasks do not need to have any entries. ---------------- task BUFFER is entry READ (V: out ITEM); entry WRITE(E: in ITEM); end BUFFER; -------------- Syntax of task bodies: task body IDENT is [declarative_part] begin sequence of statements [exception exception_handler {exception_handler}] end [IDENT]; ------------------ task body BUFFER is BUF: ITEM; begin loop accept WRITE(E: in ITEM) do BUF := E; end WRITE; accept READ (V: out ITEM) do V := BUF; end READ; end loop; end BUFFER; ------------------ Elaboration of declarations: giving effect to declarations declarations may depend on run-time value of some expression (e.g., dynamic array types, initial values for variables, etc.) elaboration: evaluate the expressions appearing in declarations. elaboration of subprogram declaration (done at scope elaboration) distinct from elaboration of declarations in its body (done when subprogram activated). Scope rules: (as in Algol 60) In a scope, declarations in current and outer scopes are visible. Declarations in current block visible after point of definition. Task entries are named like record fields: ------------------ myaccount.credit(five); ------------------ Packages are used for information hiding and explicitly say what declarations are visible from outside. types may be exported as private (name only). Exported components of package named explicitly: ------------------ stack.pop(f); ------------------ or can imported to be used without prefix of package name by a USE statement Exceptions: Exception names are declared (with static scope). Exception handlers are bound dynamically (propogate to nearest dynamic binding) Unhandled exceptions in tasks cause the task to be terminated. => an exception is not propogated outside a task. Type checking: uses name equivalance for type checking ------------------- type PERSON is record ID, AGE: INTEGER; end record; type AUTO is record ID, AGE: INTEGER; end record; W: PERSON; X: AUTO; Y: record ID, AGE: INTEGER; end record; Z: record ID, AGE: INTEGER; end record; ------------------ none of these variables can be assigned to any other. => need a single declaration (in a library unit) for each type. Tasks types are limited types (no assignment or comparison for equality) Creation of tasks: elaboration (setting up activation record) -on elaborating a task declaration (not a task type declaration) -on elaborating a variable declaration that contains some task type -on dynamic allocation activation (running): jump to statements in task body. -before first statements of body where variable declared (in parallel) (but after elaboration of all declarations) -after initialization of rest of containing object (in parallel) Only one thread of control in a task at any given time. Termination: Each task depends on a master. The master is a task, a block, a subprogram, or a library package that has declared or allocated the task. Dependency is recursive. A task is completed when it finishes its body. (ready to jump out). If there are no dependents, then a completed task is terminated. Otherwise, it terminates when all its dependents are terminated. Termination is also possible from a select statement (see below). and by abort statements (see below). Entries: primary means of communication and synchronization between tasks. entry declarations: entry IDENT [(M..N)] [formal_part]; -the range can be used to declare a family of identical entries. ------------ entry READ(1..5)(V: ITEM); ------------ entry call statement: IDENT [actual_parameter_part]; ------------ BUFFER.WRITE('c'); FOO.READ(3)(V); ------------ parameter passing same as for subprograms (in = constant, out=result, in out = value-result or reference) Accept Statement: specifies actions to be performed on call of named entry Syntax: accept IDENT [(expression)] [formal_part] [do sequence_of_statements end [IDENT]]; Examples: ------------- accept SEIZE; accept WRITE(E: in ITEM) do BUF := E; end WRITE; accept READ(3)(V: out ITEM) do V := BUF; end READ; ------------- -Formals of accept statement must match entry declaration. -Accept statemetns only allowed within task body, excluding body of nested program units and other accept statements for the same entry (or same family). -Can have more than one accept statement for same entry. Rendezvous: (as in CSP) -entry call waits for a corresponding accept statement -accept statement waits for a corresponding entry call. -when entry has been called and accept statement reached, parameters are transmitted and the body of accept run (rendezvous) -afterwards called and calling task continue in parallel Exceptions raised within accept statement (but not within nested scope) are propogated to caller of the entry and also outside the accept statement! Calls on an entry are queued. One queue for each entry. Task can call its own entries, but will then deadlock. Failures: entry call to completed task results in exception TASKING_ERROR if during rendezvous called task is aborted, caller gets TASKING_ERROR Delay statements Syntax: delay expression; -suspends task for at least the specified time. Selective waits Syntax: selective_wait ::= select select_alternative {or select_alternative} [else sequence_of_statements] end select; select_alternative ::= [when condition => selective_wait_alternative] selective_wait_alternative ::= terminate | accept_statement [sequence_of_statements] | delay_statement [sequence_of_statements] -must have at least one accept statement -can contain at most one of the following: a terminate alternative, one or more delay alternatives, an else part. Semantics: alternative is open if there is no *when* or if the corresponding condition is true, closed otherwise 1. determine open alternatives. 2. If there are open accept alternatives, then a. if a redezvous is immediately possible on one, then select one of these b. if there is no else alternative, then wait 3. a. If there are open delay alternatives, then execute one after its time has passed. (i.e., shortest delay) 3. b. If there is an else part and no accept alternatives can be immediately selected (in 2a above) or if all alternatives are closed then select the else alternative 3. c. If there is an open terminate alternative, and if -the tasks master has completed and -each task that depends on the master is already terminated or similarly waiting at an open terminate alternative, then select the terminate alternative. 4. If no else part and all alternatives closed, raise PROGRAM_ERROR exception. Conditional Entry calls: Syntax: select entry_call_statement [sequence_of_statements] else sequence_of_statements end select; -entry call is cancelled if rendezvous is not immediately possible. (actuals are always evaluated) -when cancelled, executes statements in else part Timed Entry call: syntax: select entry_call_statement [sequence_of_statements] else delay_statement [sequence_of_statements] end select; -cancels entry call if rendezvous is not started within a given time (actuals are always evaluated) Priorities: Tasks can be given priorities (through a pragma) -higher priority tasks scheduled for execution before lower ones (on same processor) -does not affect select statements Abortion: syntax: abort IDENT {, IDENT}; -prevents further rendezvous with such a task. task becomes *abnormal*, as do all its dependents -if execution of an abnormal task is suspended at an accept, select, or delay, then it is terminated. -if execution of abnormal task is suspended at entry call and not yet in rendezvous, is terminated (and removed from queue) tasks can abort any task, including itself. Task attributes: T'CALLABLE, T'TERMINATED -can task be called? is it terminated? E'COUNT -used within the body of a task with entry named E, -the number of entry calls queued on E. Shared variables: (accessible to more than one task) (tasks supposed to communicated via entry calls and accept statements) synchronization points: start and end of rendezvous (or task itself) -between two synchronization points: *if task reads a shared variable, no other can write it *if task writes a shared variable, no other can read or write -programs that do not satisfy above are erroneous (unpredictable) (but do not have to be detected) Allows tasks to cache copies of shared varibles between synch points. Ada for distributed programming? assumes shared memory, no facilities for distribution or handling failures Language assumes shared memory: -main program -single main procedure => implies limited configuration ability -initial elaboration on invocation of main procedure: all library units are elaborated, then declarations in main are elaborated. -must be done in correct order => need protocol for ensuring done in correct order. -scoping rules that allow arbitrary sharing of variables: -every access to global implies implicit communication -shared variables can be cached (will lead to program errors) -pointers can also share data accross tasks -semantics for sharing packages: -only one copy of library packages etc, shared among all tasks => library packages that have state will cause problems => concurrent I/O meaningless -timed and conditional entry calls => must negotiate with called task to back out of a call Lack of support for distribution: -no way to specify where tasks should be located (compare: object address specs, bit-level rep specs) -no way to have different reps for same type on different machines -no way to assign task names (have to know them or pass pointers) -no support for reconfiguration -no support for persistent state No facilities or semantics for failures (ref: Knight and Urquhart) No semantics for processor failure -Example: A calls B.enter, B starts to execute accept for enter, and in rendezvous B fails, -no semantics for what happens (does A wait forever?) -no way to deal with this in Ada program (timeout won't help, since already in rendezvous) -Example: suppose processor fails where there are global variables? -Example: suppose processor where main program and libraries are fails? Suggestion: make failure = abortion, but recover main program. (limit main to null statement) Suggestion: make failure in rendezvous signal exception. Conclusion: to program distributed systems in Ada have to do something more than just use Ada constructs. Alternatives: 1. can have each node be a full Ada program, communication outside Ada 2. DIADEM approach: No main program, "distributed program" is collection of "virtual nodes" virtual node = collection of library units interface of virtual node is set of interface tasks (and thus set of entries) use entry calls (rendezvous) for communication. does not handle failure! (End of Discussion of Ada per se) Liskov, Herlihy, Gilbert paper Summary: static process structure and synchronous communication is inexpressive for distributed programs. For adequate expressive power, have to abandon one or the other. Client/Server Model: Module = active, communicating entity program = collection of modules, communicating via message passing only client: makes use of services from other modules server: provides services to other modules by encapsulating resource, providing synchronization, protection, crash recovery (relies on distribution to validate model and prohibit shared state) Choices for communication: synchronous: send and recieve coupled RPC, rendezvous asynchronous: distinct send and recieve Choices for Process structure: static: fixed number (1) of threads per module dynamic: not a fixed number Combinations: Static Dynamic Synchronous Ada, DP, SR Argus, Mesa, *Mod Asynchronous CSP, PLITS, *Mod Concurrency Requirements of Client/Server Model When an activity in a module is blocked, need to do something else -otherwise get low throughput and unnecessary deadlocks example: server accepts request from client for resource, but resource is already held by another client, the other client cannot get in to release its lock so deadlock occurs -local vs. remote delay where needed resource is unavailable Local delay: To cope mechanism needs: name of called operation order in which requests are received arguments to the call, and state of the resource Adequate mechanisms if this information is available: -conditional wait (set aside activity until condition holds) -avoidance (don't accept call until local resources are available) In Ada not enough info is available (arguments to call) for avoidance to work well. Other mechanisms for Ada: Entry families: awkward, only work for static, discrete range Refusal (send exception): modularity problem (client doesn't know enough about situation) Nested accept: wrong ordering, no ultimate gain Task families: how to allocate tasks among clients? (cannot call whole family) -static assignment (ok) -choosing task to call at random ok if low contention only probabilistic guarantees -task manager adds overhead Early reply: simulate send and recieve primitives. Remote Delay (high-level server calling low-level server that finds delay): Conditional wait: high-level is still waiting Avoidance: high-level doesn't know enough to avoid delay Task families: same as above Early reply: solves problem, but inefficient and complex. higher-level server must explicitly multiplex calls. Conclusion: only general solution for avoiding delays in Ada is to simulate asynchronous primitives. Indicates lack of expressive power in Ada. Alternatives: abandon static process structure, still use RPC (as in Argus)