Home
 

Concurrency and Objects

Concurrency and Objects

Grundsätzliches

Nebenläufigkeit (tasks, protected objects) und Vererbung (extendible types) sind in Ada getrennte Konzepte. Grund: Prinzipielle semantische Schwierigkeiten, Effizienzprobleme.

$\Rightarrow$ Der Programmierer muß fallweise selbst geeignete Kombination von tasks/protected objects und Typhierachien bilden, wobei

  • Tasks/protected objects definieren allgemeines Verhaltens-/Synchronisationsschema.
  • Damit gekapselte Objektdefinitionen beinhalten Spezialisierungen, Varianten.

Offenbar 2 Möglichkeiten der Kombination.

  1. Objekt hat task/protected object als Attribut.
  2. Task/protected object ist über Diskriminante (access Typ!) mit task/protected object gekoppelt.

Grundsätzlich gibt es 2 Anwendungszwecke, mit fließenden Übergängen.

  1. Mache Objekte aktiv, d.h. statte sie mit eigenem Verhalten aus (vgl. z.B. Simula Prozesse).
    $\sim$ Prozeß + Vererbung, wobei letzters problematisch.
  2. Erlaube, daß passives Objekt in Umgebung mit Nebenläufigkeit benutzt wird, z.B. mehrere tasks greifen auf Objekt zu, d.h. statte Objekt mit Synchronisationsmechanismus aus.
    $\sim$ Protected object + Vererbung.

Wir betrachten im folgenden die zweite Zielsetzung!

Einfache Synchronisationsmechanismen für Objektzugriffe

Objekte enthalten Synchronisationsmechanismen als Attribut

Protected object ist über Diskriminante mit zu schützendem Object verbunden


package Lockable_Pack is
type Objects is abstract tagged null record; - Wurzeltyp


protected type Mutex_Plus(O: access Objects´Class) is
procedure Synchronized_Op;
end Mutex_Plus;


private
procedure Op(Object: in out Objects) is abstract;
end Lockable_Pack;


package body Lockable_Pack is
protected body Mutex_Plus is
procedure Synchronized_Op is
begin
Op(O.all);
end Synchronized_Op;
end Mutex_Plus;
end Lockable_Pack;

Anwendung dann wieder so, daß im child-package eine konkrete Version für op definiert wird.


package Lockable_Pack.Child is
type Concrete_Objects is private;


private
type Concrete_Objects is new Objects with null record;
- könnte natürlich auch was dazukommen!
procedure Op(Object: in out Concrete_Objects); - override abstract op


O: aliased Concrete_Objects; - erzeuge Objekt,
Synchronizer: Mutex_Plus(O´access); - und kopple mit mutex!
end Lockable_Pack.Child;


package body Lockable_Pack.Child is
procedure Op(Object: in out Concrete_Objects) is
begin
...
end Op;
end Lockable_Pack.Child;

Großer Nachteil der Methode: Zu Beginn, d.h. bei Definition des protected objects ,,mutex_plus`` muß Anzahl (und Art) der Operationen (Methoden) der Objektfamilie schon feststehen - man kann später nichts mehr hinzufügen zu dein Operationen des protected_objects.

Was bleibt dann bei dieser Methode noch für Typerweiterung übrig? Man kann:

  • Beteiligte Datentypen variieren (z.B. Keller für Integer, Char, ...= generics!)
  • Implementierung variieren (z.B. Keller durch array, linked list)
undeinem

,,Bounded Buffer`` im Stil 10.2.2 (Code aus Burns/Wellings)


package Buffers is
type Data abstract tagged null record; - lasse Elementtyp offen
type Bufer is abstract tagged private; - lasse Implementierung offen


- Basic protected Buffer
protected type Buffer_Controller(B: access Buffer´Class) is
entry Put(D: in Data´Class); - entries, weil condition snychro
entry Get(D: out Data´Class);
end Buffer_Controller;


procedure Put(D: Data´Class; B: access Buffer) is abstract;
procedure Get(D: Data´Class; B: access Buffer) is abstract;
function Buffer_Not_Full(B: access Buffer) return Boolean is abstract;
function Buffer_Not_Empty(B: access Buffer) return Boolean is abstract;


private
type Buffer is abstract tagged null record;
end Buffers;


package body Buffer is
protected body Buffer_Controller is
entry Put(D: in Data´Class) when Buffer_Not_Full(B) is
- note Buffer_Not_Full(B) is a dispatching operation
begin
Put(D, B); - dispatching operation
end Put;


entry Get(D: in Data´Class) when Buffer_Not_Empty(B) is
- note Buffer_Not_Empty(B) is a dispatching operation
begin
Get(D, B); - dispatching operation
end Get;
end Buffer_Controller;
end Buffers;

Jetzt Typerweiterung: Puffer soll als Vektor realisiert werden und kriegt hier entsprechende Attribute.


package Buffers.Array_Based_Buffers is
type Array_Buffer is abstract new Buffer with private;
Buffer_Size: constant Natural := 10;


procedure Put(D: Data´Class; B: access Array_Buffer) is abstract;
procedure Get(D: out Data´Class; B: access Array_Buffer) is abstract;


function Buffer_Not_Full(B: access Array_Buffer) return Boolean is abstract;
function Buffer_Not_Empty(B: access Array_Buffer) return Boolean is abstract;


private
- The package could be made generic, and the size passed as a generic parameter
subtype Index is mod Buffer_Size;
subtype Count is Natural range 0..Buffer_Size;


type Array_Buffer is abstract new Buffer with record
First: Index := Index´First;
Last: Index := Index´First;
Number_In_Buffer: Count := 0;
end record;
end Buffers.Array_Based_Buffers;

Hier noch als abstrakt, keine Implementierung, da Elementtyp noch nicht feststeht.

Zwischenschritt ist sinnvoll, wenn man array-basierte Puffer für verschiedene Elementtypen haben will.

Implementierung: Integer Elemente


package Buffers.Array_Based_Buffers.Integer_Buffers is
type Integer_Data is new Data with record
X: Integer;
end record;


type Integer_Array_Buffer is new Array_Buffer with private;


private
- a bounded buffer


type Integer_Array is array(Index) of Integer_Data;
- child package has visibility of the private part of the parent


type Integer_Array_Buffer is new Array_Buffer with record
Mb1: Integer_Array;
end record;


procedure Put(D: Data´Class; B: access Integer_Array_Buffer);
procedure Get(D: out Data´Class; B: access Integer_Array_Buffer);


function Buffer_Not_Full(B: access Integer_Array_Buffer) return Boolean;
function Buffer_Not_Empty(B: access Integer_Array_Buffer) return Boolean;
end Buffers.Array_Based_Buffers.Integer_Buffers;

Implementierung:


package body Buffers.Array_Based_Buffers.Integer_Buffers is
procedure Put(D: Data´Class; B: access Integer_Array_Buffer) is
begin
B.Mb1(B.Last) := Integer_Data(D); - may generate Constraint_Error
B.Last := B.Last +1;
B.Number_In_Buffer := B.Number_In_Buffer +1;
exception
when Contraint_Error =>
- potential error recovery
raise;
end Put;


procedure Get(D: out Data´Class; B: access Integer_Array_Buffer) is
begin
D := Data´Class(B.Mb1(B.First));
B.First := B.First +1;
B.Number_In_Buffer := B.Number_In_Buffer -1;
end Get;


function Buffer_Not_Full(B: access Integer_Array_Buffer) return Boolean is
begin
return B.Number_In_Buffer = Buffer_Size;
end Buffer_Not_Full;


function Buffer_Not_Empty(B: access Integer_Array_Buffer) return Boolean is
begin
return B.Number_In_Buffer /= 0;
end Buffer_Not_Empty;
end Buffers.Array_Based_Buffers.Integer_Buffers;

Eleganz der Gesamtkonzeption ist wie oft bei OO verbunden mit Laufzeit-Unsicherheit: Weil Put/Get für Datenelemente Parameter von klassenweiten Typ haben, ist Aufruf mit anderem (von Data abgeleitetem) Typ ebenfalls möglich.

Inheritance Anomaly

  • 10.2.2 verlangt, daß vorweg klar ist, welche Operationen der Synchronisation durch das protected object unterworfen werden sollen,
  • 10.2.1 erlaubt zwar, weitere Operationen hinzuzufügen, ermöglicht aber keine freie Gestaltung/Änderung/Bedingungssynchronisation.

In dem in 10.2.1 beschriebenem Verfahren kann man beliebig viele Operationen op2, op3 usw. dazunehmen und dem wechselseitigen Ausschluß unterwerfen (oder eben nicht), aber es ist unmöglich, daß z.B.

  • op1 muß allein sein, op2, op3 zusammen.
  • Zustandsabhängige Synchronisation/Ausschluß (z.B. Puffer voll/leer).

Solche Änderungen sind nur unter Aufhebung des Geheimnisprinzips und Änderung früher definierter Operationen möglich. = Inheritance anomaly!

Objekte mit Rumpf (body)

Rumpf kontrolliert Methodenausführung, vgl. Ada: Service_Task (= Endlosschleife mit selektivem accept).

Dabei kann dann akzeptierter Methodenaufruf konkurrent oder innerhalb des Rumpf-Prozesses ausgeführt werden.

Offenbar: Für jede Unterklasse, die neue Methoden einführt oder Synchronisationsbedingungen ändert, muß body neu definiert werden.

Akzeptanz-Mengen (,,enabled sets``)

  • wechselseitiger Ausschluß unterstellt
  • Bezeichner für enabled sets

Wächter (guards) für Methoden

Anwendung auf Beispiel: Klar, z.B. procedure Get(...) when (In >= Out +1) is ...

Kann Fälle wie ,,Get2`` vereinfachen, wichtig auch hier: Guard überschreibbar?

Aber: Methode versagt, wenn vorhandene Zustandsgrößen die gewünschte Bedingung nicht auszudrücken vermögen.

Typisches Beispiel: Zustand = Details der Vor-Geschichte, z.B. Reihenfolge früherer Methodenaufrufe.

Natürlich gilt immer: Zustand = Akkumulierte Vorgeschichte, aber tatsächliche Wahl von Zustandsbegriff immer bezogen auf Funktion, d.h. Abstraktion/Äquivalenzklassenbildung; Vorgeschichte = feinster Zustandsbegriff.