Programmierung

Multithreading – Parallelisierung und Nebenläufigkeit in CONZEPT 16

Seit der Version 5.6.06 können auch die grafischen Clients rechen- und zeitintensive Verarbeitungen im Hintergrund durchführen. Zuvor war diese Möglichkeit dem SOA-Service vorbehalten. Nachdem das Thema im kürzlich veranstalteten “Best Practice”-Workshop vorgestellt wurde, möchte ich Ihnen hier die Programmierung anhand von Beispielen erläutern.

Über sogenannte Jobs können Teile der Verarbeitung in den Hintergrund ausgelagert werden, ohne die Applikation während der Laufzeit zu blockieren. Jobs laufen in einem unabhängigen Kontext und verfügen unter anderem über eine eigene Datenbankverbindung, einen eigenen Benutzer und eigene Deskriptoren. Ein Job wird in einem separaten Thread gestartet oder im SOA-Service auch als eigenständiger Prozess.

Job-Starter

Gestartet werden können Jobs mit der Funktion JobStart(). Dabei muss übergeben werden, ob der Job als Thread oder als Prozess ausgeführt und in welcher
Prozedurfunktion er laufen soll. Außerdem kann ein Zeitraum definiert werden, in dem der Job nach dessen Durchführung noch für das Anlegen eines Kontrollobjektes
verfügbar bleibt. Zusätzlich können dem Job Daten und eine Beschreibung als Zeichenkette übergeben werden, die innerhalb der Funktion verarbeitet werden können.

Ist der Job erfolgreich gestartet worden, gibt die Funktion eine positive ID zurück, die innerhalb des Prozesses eindeutig ist. Ist der Zeitraum in dem der Job nach der Durchführung noch verfügbar ist sehr kurz gewählt oder auf 0 gesetzt, kann auch der “Fehler” _ErrTerminated zurückkommen. In diesem Fall wurde der Job bereits durchgeführt und beendet.

Beispiel
sub JobStarter
()

  local
  {
    tJobID : int;
    tErr   : int;
  }

{
  // Job als Thread starten und bis 30 Sek. nach Durchführung
  // verfügbar bleiben
  tJobID # JobStart(
    _JobThread,        // Jobtyp
    30,                // Verfügbarkeitszeitraum nach Durchführung
    __PROC__ + ':Job', // Prozedurfunktion
    'myData',          // Daten
    'myDescription'    // Beschreibung
  );
  // Job läuft
  if (tJobID > 0)
  {
    // weitere Verarbeitung, zum Beispiel Kontrollobjekt anlegen
    // ...
  }
  // Job läuft nicht (mehr)
  else
  {
    tErr # tJobID;

    tJobID # 0;

    // Job bereits erfolgreich durchgeführt
    if (tErr = _ErrTerminated)
      tErr # _ErrOK;
  }

  // Fehler zurückgeben
  return(tErr);
}

Job

Der Job startet direkt nach dem Aufruf von JobStart() in der angegebenen Prozedurfunktion. Die aufrufende Prozedur wird parallel dazu fortgesetzt. Die Job-Funktion bekommt als Argument ein Job-Objekt übergeben, das über Eigenschaften unter anderem den Zugriff auf die übergebenen Daten und die Beschreibung erlaubt.

Neben dem Job-Objekt wird ein weiteres Argument übergeben, dass aber ohne Bedeutung ist und immer den Wert 0 hat.

Beispiel
sub Job
(
  aJob : handle; // Job-Objekt
  aEvt : int;    // ohne Bedeutung
)

  local
  {
    tData        : alpha;
    tDescription : alpha;
  }

{
  // Daten ermitteln
  tData # aJob->spJobData;
  // Beschreibung ermitteln
  tDescription # aJob->spSvcDescription;

  // Tue etwas Sinnvolles
  // ...
}

Bei Jobs mit einer erwarteten Laufzeit über einige wenige Sekunden hinaus, sollte die Eigenschaft spStopRequest abgefragt werden. Sie wird gesetzt, wenn der Job beendet werden soll. Dies ist dann der Fall, wenn der Client beendet wird, oder ein Kontrollobjekt das Beenden des Jobs anfordert.

Über die Eigenschaft spJobStatus kann der Job seinen Status bekannt geben. Die Eigenschaft ist vom Typ int (ganzzahlig, 32 Bit). Der Wert kann vom Programmierer frei gewählt werden.

Beispiel
sub Job
(
  aJob : handle; // Job-Objekt
  aEvt : int;    // ohne Bedeutung
)

  local
  {
    tStatus : int;
  }

{
  // Tue etwas Sinnvolles bis du gestoppt wirst
  do
  {
    // ...

    // Status setzen
    aJob->spJobStatus # tStatus;
  }
  while (aJob->spStopRequest = false);
}

Anstatt für jede Verarbeitung einen neuen Job zu starten, besteht auch die Möglichkeit einen Job mit der Funktion JobSleep() in einen Wartezustand zu versetzen. Bei Bedarf kann der Job über ein Kontrollobjekt aufgeweckt werden, um die Verarbeitung zu starten.

Beispiel
sub Job
(
  aJob : handle; // Job-Objekt
  aEvt : int;    // ohne Bedeutung
)

  local
  {
    tStatus : int;
  }

{
  // Warte 60 Sekunden etwas Sinnvolles tun zu können,
  // bis du gestoppt wirst
  while (!JobSleep(60 * 1000) and aJob->spStopRequest = false)
  {
    // ...

    // Status setzen
    aJob->spJobStatus # tStatus;
  }
}

Kommunikation

Mit der Funktion JobOpen() kann für einen laufenden Job ein Kontrollobjekt angelegt werden. Über dieses Objekt kann mit dem laufenden Job interagiert werden. Ein Kontrollobjekt kann vom Starter des Jobs oder aber auch von anderen Jobs angelegt werden. Dabei ist zu beachten, das es zu einem Job immer nur ein Kontrollobjekt geben kann.

Die Eigenschaft spJobErrorCode enthält den Wert eines eventuell innerhalb der Prozedurfunktion des Jobs aufgetretenen Fehlers. Über die Eigenschaft spJobStatus kann der vom Job gesetzte Status abgefragt und auch gesetzt werden. Diese Eigenschaft stellt somit eine sehr einfache Möglichkeit zur bidirektionalen Kommunikation mit einem Job da.

Beispiel
sub JobController
(
  aJobID : int;
)
: int;

  local
  {
    tJobControl : handle;
    tErr        : int;
  }

{
  // Kontrollobjekt anlegen
  tJobControl # JobOpen(aJobID);
  if (tJobControl > 0)
  {
    // Fehlerwert abfragen
    tErr # tJobControl->spJobErrorCode;
    // Status abfragen
    tStatus # tJobControl->spJobStatus;

    // ...

    // Status setzen
    tJobControl->spJobStatus # tStatus;
    // Kontrollobjekt freigeben
    tJobControl->JobClose();
  }
  else
    tErr # tJobControl;

  // Fehler zurückgeben
  return(tErr);
}

Um einen laufenden Job zu beenden oder aufzuwecken kann die Funktion JobControl() verwendet werden.

Beispiel
sub JobController
(
  aJobID : int;
)
: int;

  local
  {
    tJobControl : handle;
    tErr        : int;
  }

{
  // Kontrollobjekt anlegen
  tJobControl # JobOpen(aJobID);
  if (tJobControl > 0)
  {
    // Job aufwecken
    tErr # tJobControl->JobControl(_JobWakeup);

    // ...

    // Job beenden; Ende nicht abwarten
    tErr # tJobControl->JobControl(_JobStop);

    // ...

    // Job beenden; Ende abwarten
    tErr # tJobControl->JobControl(_JobTerminate);

    // Kontrollobjekt freigeben
    tJobControl->JobClose();
  }
  else
    tErr # tJobControl;

  // Fehler zurückgeben
  return(tErr);
}

Für eine intensivere Kommunikation besteht außerdem die Möglichkeit MSX-Nachrichten zwischen dem Job- und dem Kontrollobjekt auszutauschen. Dieser Thematik widmen wir uns in einem folgenden Artikel.


Wir planen die Kommunikation zwischen Job und Job-Starter um eine Ereignisfunktion zu erweitern. Der Job kann darüber den Job-Starter über eine Statusänderung, zum Beispiel beim Beenden des Jobs, informieren. Der Job-Starter kann darauf reagieren und beispielsweise dem Anwender eine Meldung anzeigen.

7 Kommentare

7 Kommentare “Multithreading – Parallelisierung und Nebenläufigkeit in CONZEPT 16”

  1. Auf jeden Fall positiv, das man Start und Ablauf überwachen kann.
    Was passiert, wenn der Job z.B. mit einem Laufzeitfehler abstürzt ? Kann der Elternprozess das feststellen, oder erhält er nur "Terminated" ?

  2. Ja, im Moment müsste man den Job von außen regelmäßig nach seinem Status fragen, z.B. mit einem Timer.

    Und ja, das Pluszeichen hat sich wohl irgendwo anders hingeschlichen.

  3. Klingt interessant. Mit den geplanten Ereignisfunktionen dürfte dieses Verfahren noch nützlicher sein, da man dann auf das Ende einer aufwändigen Berechnungen per Ereignis aufmerksam gemacht wird, während man jetzt bei der Programmierung der "Job-Verwertung" die potentielle Laufzeit und den Verfügbarkeitsrahmen berücksichtigen muss, wenn ich das richtig verstanden habe.

    Anm.: Im ersten Beispiel soll es sicherlich "__PROC__ + ‘:Job’" statt "__PROC__ ‘:Job’" heissen…

Kommentar abgeben