Concurrency and Objects
Concurrency and Objects
Grundsätzliches
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.
- Objekt hat task/protected object als Attribut.
- Task/protected object ist über Diskriminante (access Typ!) mit task/protected object gekoppelt.
Grundsätzlich gibt es 2 Anwendungszwecke, mit fließenden Übergängen.
- Mache Objekte aktiv, d.h. statte sie mit eigenem Verhalten aus (vgl. z.B. Simula Prozesse).
Prozeß + Vererbung, wobei letzters problematisch. - 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.
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)
,,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.