CRuntimeJavalike - Ausnahmebehandlung - Exceptions

CRuntimeJavalike - Ausnahmebehandlung - Exceptions

Inhalt


1 Grundsätzliches, Konzepte

Topic:.Exception_Jc.errorTolerance.

Sind Softwarefehler auszuschließen? Als Ziel eines guten Softwareengeneering sollte dies gelten. Aber nicht als Betrachtung von Tatsachen. Softwarefehler sollten minimiert werden. Wichtiger ist es, dass in beliebigen Situationen ein Softwaresystem deterministisch und stabil reagiert.


1.1 Fehler oder Ausnahmen?

Topic:.Exception_Jc.Whatis.

Aus Sicht einer einzelnen Subroutine eines Programmes kann ein Fehler oder Ausnahme nur wie folgt beschrieben werden: Die Subroutine hat eine Aufgabe zu erledigen und findet Bedingungen vor, die eine ordentliche Erledigung der Aufgabe nicht zulassen. Mehr kann in diesem Kontext nicht festgestellt werden. Makroskopisch, aus Sicht des Bedieners des gesamten Programmes, ist dagegen konkreter aussagbar: Programm funktioniert nicht, oder Absturz. Die Bedingungungen, dass es zu dieser Situation kommt, ist teilweise makroskopisch bedingt, teilweise ist ein lokaler Softwarefehler die Ursache, aber nicht in dem Modul, in dem dann die Ausführungsbedingung nicht passt. Ein Fehler oder eine Ausnahme kann softwaretechnisch nur lokal in einem Kontext festgestellt werden, muss aber übergreifend beachtet und behandelt werden.

Die Frage ist, ob man etwas als Fehler oder als ein erwartbarer aber nicht normaler Zustand bezeichnen sollte. Wenn ein File geöffnet werden soll, dieser File aber nicht an dieser Stelle vorhanden ist, dann kann das ein grober Fehler sein, etwa wenn eine Installation eines Tools defekt ist. Es kann aber auch normal sein. Aus Sicht des file open kann das nicht bewertet werden: Fehler oder Ausnahme.

Im Softwareengeneering-Bereich hat sich daher der Begriff Ausnahme (Exception) etabliert anstatt von Fehler (Error). Man kann diese Begriffswandlung an der Entwicklung des try-catch-throw-Konzeptes festmachen. Es ist aber nicht so, dass es bei try-catch-throw um Ausnahmen ginge, bei der klassichen Fehlerbehandlung in C via errno usw. dagegen um Fehler. Die Sache ist ein und dieselbe. Im folgenden Text wird der Begriff Ausnahme verwendet, wenn es um Softwareengeneering-Betrachtungen geht. Fehler meint wirkliche beseitigungswürdige Fehler.


1.2 Abbruch oder Auffangen?

Topic:.Exception_Jc.AbortOrCatch.

Bezüglich der Reaktion auf Ausnahmen sind zwei polare Verhaltensweisen programmierbar, die beide ihre Berechtigung haben:

  • Bei einer Ausnahme wird so gut wie möglich fortgesetzt.

  • Bei einer Ausnahme wird das gesamte Programm mit einer Fehlermeldung abgebrochen.

  • Ersteres Verhalten ist beispielsweise bei Internet-Browsern zu beobachten: Auch wenn der Input-html-Text fehlerhafte End-Tags enthält, oder bei <li> das zugehörige <ul> fehlt, fehlende Attribute werden mit Standardbesetzungen ersetzt usw., es wird so gut wie möglich ein Text dargestellt. Nur mit dieser Herangehensweise sind Browser verschiedener Hersteller mit unterschiedlichen Features oder alte und neue Browser bei neuen oder alte html-Inputs (Internetseiten) beherrschbar.

    Das zweite Verhalten, Gesamt-Abbruch, ist beispielsweise bei Anlagensteuerungen anzutreffen. Hier läuft abgestimmte Software immer in gleichen Algorithmen. Ein Fehler wird schlichtweg nicht erwartet, wenn ein Fehler passiert, muss er erkannt und möglichst umgehend korrigiert werden. Allerdings ist es schon fatal, wenn die gesamte Steuerung steht, nur weil in einer Protokollierung unter nicht betrachteten Umständen sich doch ein Softwarefehler eingeschlichen hat oder eine Bedingung nicht beachtet wurde. Konsequent gesagt: Ein solches Verhalten ist nicht mehr zeitgemäß da die Softwaresysteme heute meist komplexer sind. Eine Ausnahme muss auf der Ebene eines Moduls abfangbar sein. Das Modul kann gegebenenfalls in einen Grundzustand überführt werden, aber die Gesamt-Software muss geeignet deterministisch reagieren.

    Mit dem erst genannten Verhalten werden Ausnahmebedingungen, oder härter formuliert Fehler, nicht erkannt bzw. vertuscht. Das ist ok, wenn es darum geht, dass etwas zunächst vordergründig überhaupt funktionieren soll. Wenn man in C programmiert, wenige Debugmöglichkeiten beispielsweise in einem speziellen Zielsystem hat, man nicht wegen einem unbeachteten Falles Dinge tun will die man im Moment nicht vorhat, dann ist man schonmal geneigt, einen gut-Zustand hineinzuprogrammieren. Wenn beispielsweise ein Array-Index von außen kommt, dieser wird getestet ob er im zulässigem Bereich liegt: Was tun, wenn der Index unerwarteterweise nicht stimmt? Erstmal auf 0 setzen, damit auf keinen Fall im eigenen Algorithmus ein Folgefehler passiert. Dass dann falsche Daten verwendet werden, ist erstmal nicht wichtig. Jetzt nicht mit diesem Problem befassen ist ein durchaus berechtigtes Ansinnen. Doch bei einer späteren Feinbehandlung des Programmes sollte eine passende Behandlung eingebaut werden.

    Es ist möglich, bei sonst nicht behebbaren Ausnahmen in einem Softwaresystem die zugehörige Hardware rückzusetzen und neu anlaufen zu lassen. Das behebt jedenfalls Ausnahmen, die aufgrund von Zuständen in der Software selbst verursacht worden sind. Beispielsweise wenn kein Speicher mehr reserviert werden kann, weil ein Prozess einmalig Speicher angefordert hat und aufgrund anderer Zusammenhänge nicht wieder freigibt. Das sich der Softwarefehler wieder einstellt, ist gegebenenfalls erst nach einiger Zeit zu erwarten. Dann ist Reparator notwendig. Erstmal läuft das System wieder. Insbesondere ist das bei wenig komplexen oder autarken, langläufigen oder bedienfreien Systemen angemessen.


    1.3 Traditionelle Ausnahmebehandlung in C

    Topic:.Exception_Jc.ErrorHandlingClassic_C.

    Traditionell wird in C, auch in C++ eine Fehlerbehandlung entweder mittels Abfrage der Returnwerte aufgerufener Funktionen durchgeführt, möglicherweise werden dabei auch positive Werte als Rückgabe einer Information (Byteanzahl oder dergleichen) benutzt, negative Werte zeigen einen Fehler an:

    int error = callFunction(something);
    if(error != 0){ .... }
    

    Gegebenenfalls steht ein Fehlercode in einer spezielle (System-) Errorvariable. Das ist das Konzept der low-level-IO-Funktionen im traditionellen Standard-C:

    int bytes = fread(fileHandle);
    if(bytes < 0){ printf("file erroe: %i", errno) }
    

    Diese Art der Programmierung ist als klassisch zu bezeichnen. Das Problem hierbei ist, dass die aufrufende Ebene immer die Problematik des Tests auf Fehler berücksichtigen muss. Auch wenn in der unmittelbar aufrufenden Ebene keine sinnvolle Behandlung möglich ist. Der Fehler muss dann durchgereicht werden. Das ist Programmieraufwand. Wird in einer ausführenden Ebene eine Fehlerrückkehrbedingung neu eingebaut, dann müssen alle aufrufenden Ebenen nachgebessert werden.


    1.4 Fehlerbehandlung mit try-catch-throw in C++ und Java

    Topic:.Exception_Jc.ErrorHandlingTry.

    In C++, in Java und auch in fast allen modernen Sprachen gibt es dagegen die try-catch-throw-Methodik:

    try{ callFunction(something); }
    catch(SpecialException exception){ .... }
    ...
    callFunction(args)
    { if(I_cannot_do_that){ throw new SpecialException("infotext"); }
    }
    

    Der wesentliche Vorteil dieser Methodik ist, dass Fehler über mehrere Subroutinen-Ebenen durchgereicht werden können, ohne dass in diesen zwischenliegenden Subroutinen (Methoden) auf den Fehlerfall Rücksicht genommen werden muss. Damit wird nicht nur Programmierarbeit (Fleiß) gespart, sondern es können sich auch keine Fehler bei der Fehlerbehandlung einschleichen. Man muss dazu bedenken, dass ein Programmierer oft zunächst nur Augenmerk auf den "Gut-Fall" legt, das Abfangen aller möglichen und unmöglichen Ausnahmezustände dagegen als lästig betrachtet werden.

    In Java wird man bei allen Exceptions außerhalb von RuntimeException darauf hingewiesen, dass mit einer Ausnahme gerechnet werden muss:

    void methodDoSomething throws SpecialException  { callFunction(something);    ...  }
    

    Man braucht sich in einer Zwischenebene zwar nicht programmtechnisch um die Exceptionbehandlung kümmern, wenn die Exception dort nicht abfangbar ist. Aber man muss in der Deklaration der Methode spezifizieren, dass eine solche Exception durchgereicht, gegebenenfalls von inneren Routinen erzeugt werden kann. Der Java-Compiler weißt mit einem Compilerfehler eindeutig darauf hin wenn diese throws-Angabe fehlt. In Eclipse beispielsweise hat man es mit "Quik fix" auch sehr leicht, die notwendige Korrekturen anbringen (zu lassen). Dieses Konzept von Java wird zwar auch als Umstritten angesehen, weil eben die Berücksichtigung einer Ausnahme syntaktisch erzwungen nun doch wieder in Zwischenebenen erfolgen muss. Aber andererseits handelt es sich um eine Änderung der Schnittstelle einer Subroutine, wenn es jetzt eine Ausnahme geben kann, und vorher nicht. Man sollte sich also gedanklich in den aufrufenden Ebenen damit auseinandersetzen. Das der Compiler dazu zwingt, ist nur gut so.

    Bei C++ kann man das "throws" nicht angeben. Damit weiß man als Programmierer erstmal nicht, dass eine Exception möglicherweise auftreten kann und sinnvollerweise abgefangen oder beachtet werden sollte. Man muss genau die Dokumentation möglicherweise auch aller innen gerufenen Subroutinen anschauen, um die richtigen programmtechnischen Schritte zu tun. Dokumentation kann lückenhaft sein.


    1.5 Stacktrace in Java

    Topic:.Exception_Jc.StacktrcJava.

    Passiert in Java ein throw, dann kann die Fehlerstelle im Quellprogramm recht leicht lokalisiert werden, in dem man sich den Stacktrace ausgeben lässt:

    try{ doSomething(); }
    catch(ExceptionX exc)
    { exc.printStackTrace(System.out);
    }
    

    Die Ausgabe dazu sieht beispielsweise wie folgt aus:

    java.lang.ArrayIndexOutOfBoundsException: 3
      at vishia.example.common.TestTry.execute(TestTry.java:17)
      at vishia.example.common.TestTry.main(TestTry.java:11)
    

    Es wird bei dieser Ausgabe die Fehlerart und die gerufenen Methoden einschließlich Angabe des Quellfiles und der Zeilennummer angegeben. Damit ist für einen Entwickler ein Fehler oft leicht lokalisierbar. Auch wenn ein Fehler in einem Kundenprojekt passiert, kann diese Ausgabe anstatt auf den Bildschirm in einen logfile geschrieben werden. Angenommen, der Fehler lässt sich kundenverträglich auffangen, wird er im folgenden Release korrigiert, ohne dass die Gesamtfunktionalität wesentlich leidet. Gegebenenfalls muss der Kunde als Workarround lediglich bestimmte Bediensituationen vermeiden.

    Dem Stacktrace kann also ein sehr hoher Stellenwert bei der Fehlerbehandlung zugeschrieben werden, insbesondere dann wenn es um Betatests, Nullserien oder Anlagen-Software als Einzelstücke geht. Ein solcher Stacktrace wäre bei C (++)-Anwendungen auch sehr nützlich.


    2 Try-catch-throw-Themen

    Topic:.Exception_Jc.TryCatchThrowinC.

    Es gibt selbst einige C-Compiler, die das try-catch-Konzept kennnen. Andererseits ist auch bei C++-Programmierern die Verwendung des try-catch-throw-Konzeptes nicht geläufig. Gründe dafür liegen vielleicht in der vielfältigen Nichtdurchschaubarkeit der C++-Konstrukte, unterstellt mit "man weiß nicht was da so alles abläuft". Tatsächlich ist der Maschinencode, der bei throw durchlaufen wird, nicht gerade durchschaubar. Dazu kommt, dass die Implementierung in einem Zielsystem von der Qualität des zugehörigen Compilers abhängt, bei der Vielzahl von Prozessorfamilien ist nicht jeder diesbezüglich in allen Konstellationen bestens getestet. Folglich gibt es auch durchaus auch eine Ablehnung dieses moderenen Fehlerbehandlungskonzeptes.

    In Java sieht das anders aus: Dort ist das Fehlerbehandlungskonzept fester Sprachbestandteil und in Standardbibliotheken fest verankert. Dazu kommt einerseits die Einschränkung der Exception-Objekte basierend auf einer klare Basisklasse der Fehlerbehandlung java.lang.Excpetion und die Klarheit der Fehlerbehandlung mit der throws-Deklaration für Methoden. Damit kommt man bei Java nicht einmal auf die Idee, das Exception-Konzept nicht zu benutzen.

    In C/C++ muss die Vielfalt der Zielsystemcompiler beachtet werden. Außerdem soll hier betrachtet werden, wie man in C mit dem try-catch-throw-Konzept einschließlich Stacktrace hinkommt. Daher folgend eine Betrachtung von Detailproblemen:


    2.1 finally ?

    Topic:.Exception_Jc.TryCatchThrowinC..

    Java und auch andere moderen Programmiersprachen kennen das finally-Konstrukt:

     doOpenThings();
     try{ doSomething(); }
     catch(ExceptionX exc){ processExceptionX(); return; }
     catch(ExceptionY exc){ processExceptionY(); System.exit(1);}
     finally{ doClosingThings(); }
    

    Im Beispiel wird in einer Methode etwas angelegt, geöffnet und dergleichen. Danach folgt ein Block, der gegebenenfalls eine Exception wirft. Diese wird abgefangen. Der finally-Block wird immer durchlaufen, auch wenn keine Exception vorliegt, auch wenn im catch-Block ein return erfolgt. Im finally-Block können die Dinge, die im try-Block angelegt, geöffnet und dergleichen worden sind, wieder freigegeben, geschlossen und dergleichen werden.

    In C++ fehlt die Möglichkeit der Angabe eines finally-Blockes. Dafür werden aber die Destruktoren der entsprechenden Ebene abgearbeitet. Man könnte meinen, das wäre genug um diese "ClosingThings" abzuarbeiten. Erfasst werden aber dabei nicht die Dinge außerhalb der angelegten Instanzen. Das Konzept der Destruktoren reicht also nicht. C++ ist die entwicklungsgeschichtlich ältere Programmiersprache.


    2.2 Ausnahme-Objekte

    Topic:.Exception_Jc.TryCatchThrowinC..

    In Java sieht das throw meist wie folgt aus:

     throw new SpecialException("text" + value);
    

    Es wird also ein Ausnahme-Objekt neu im Heap angelegt. Dort drinnen stehen Informationen zur Ausnahme. Automatisch übertragen wird dort hinein auch der Stacktrace. Das Ausnahmeobjekt kann dann im catch verwendet werden. Nicht direkt sichtbar wird im Heap auch ein StringBuffer angelegt, wenn wie im Beispiel ein verketteter String aufgebaut wird.

    Das Neu-Anlegen eines Ausnahmeobjektes ist nicht Pflicht. Man kann auch auf eine vorbereitete Instanz zurückgreifen, für die dann allerdings die Zugriffsrechte (Mutex, von einem anderen Thread eben auch gerade benutzt?) beachten muss. Für ein Java-Programm ist das eine unnötige Erschwernis. Bei Echtzeitsteuerungen ist das aber aus mehreren Gründen zu erwägen:

    C++ kann in seinem throw-Statement nicht nur Referenzen auf Ausnahme-Objekte aufnehmen, sondern alles Mögliche. Hier zeigt sich C++ flexibler aber auch etwas schlecht durchschaubar.


    2.3 Verwenden von longjmp in C

    Topic:.Exception_Jc.TryCatchThrowinC..

    Das longjmp-Konzept gibt es schon seit der Anfangszeit von C:

    #include <setjmp.h>
    void subroutine(jmp_buf);
    void routine(void)
    { int value;
      jmp_buf jumper;
      value = setjmp(jumper);
      if (value != 0)
      { printf("Longjmp with value %d\n", value);
        exit(value);
      }
      subroutine(jumper);
    }
    void subroutine(jmp_buf jumper)
    { ...
      longjmp(jumper,1);
    }
    

    Das Prinzip ist, das in der Datenstruktur jumper eine Adresse und gewisser Stackinhalt gespeichert ist. Der Aufruf von longjmp führt zum Zurückstellen des Stack auch über mehrere Aufrufebenen hinweg. Es wird die gleiche Stelle angesprungen, die in der Routine setjmp vermerkt worden ist, und es wird damit so getan, als würde man zum zweiten mal aus setjmp zurückkehren. Der Unterschied ist, bei direktem Aufruf von setjmp immer mit dem Rückgabewert 0, sonst mit dem Rückgabewert, der bei longjmp angegeben wurde. Das lässt sich dann zur Verzweigung nutzen.

    Mit diesem Konzept kann man in C das throw ersetzen, dass über mehrere Subroutinenebenen bis zum nächsten catch springt. Zu beachten ist allerdings, dass damit Destruktoren der Zwischen-Subroutinen übergangen werden, also untauglich für C++, aber gut tauglich für C: Da von longjmp Subroutinenebenen überspringen werden, erfahren diese Subroutinen auch nichts von dem Fehleraussprung. Wenn in diesen Subroutinen bestimmte Dinge vor dem return als Abspann getan werden müssten, dann werden diese nicht getan, da die Subroutinen vom Fehleraussprung überhaupt nicht berührt werden. In C++ gibt es das Konstruktor/Destruktor-Konzept: Von Daten, die im Stack angelegt werden, werden ohne bewußte Programmierung bei Verlassen der Stackebene der Destruktor aufgerufen. In vielen Fällen ist der Destruktor leer, also unnötig, doch in anderen Fällen muss irgendetwas getan werden, ein Speicherbereich freigegeben, eine Verzeigerung gelöst, ein Filehandle geschlossen und sonst was. Wenn ein Programmierer darauf vertraut, das beim Verlassen einer Subroutinen notwenige Abspannmassnahmen, die er programmiert hat, auch ausgeführt werden, dann hat er sich bei Verwendung von longjmp als Fehleraustritt geirrt. Das kann fatal sein, da der Programmierer gegebenenfalls gar nicht wissen kann, dass weiter drin in einer gerufenen Subroutine ein longjmp ausgeführt wird, das zwar weiter draußen beachtet wird, aber er ist nicht involviert / informiert.

    Hier hilft aber ein bewußtes try-finally in den Zwischenebenen, wie in den folgenden Beispielen noch gezeigt wird.


    3 Stacktrace in C/C++

    Topic:.Exception_Jc.StacktrcCpp.


    In Java hängt an jedem Exception-Object ein Stacktrace. Der Stacktrace liefert Informationen, in welcher Routine an welcher Programmstelle die Ausnahme passiert ist, und aus welchen Routinen der Aufruf erfolgte.

    Man stelle sich vor, ein Programm ist getestet und an Kunden ausgeliefert. Unter bestimmten Umständen kommt es aber zu Ausnahmen. Beispielsweise kann ein Messwertaufnehmer defekt sein, daher liefert der zugehörige Treiber ungültige Werte. Ein solche Situation sei aber beim Softwaretest erfasst, also nicht fehlerträchtig. Nehmen wir an, wegen dem Messwertaufnehmer-Fehler wird ein Prozesswert auf 0 oder auf einen infiniten Wert gesetzt. Eine weitere Berechnung wird normalerweise nicht ausgeführt. Doch in im Beispielszenario kommt es in einer Sonderbedingung doch zu einer Verwendung dieser an sich unstimmigen Werte, Folge ist ein Division durch 0 oder ein falscher Index bei Arrayzugriffen, und schon gibts die Ausnahme. Das ist ein konstruiertes von möglichen Beispielen. Fehler passieren nicht im Linearfall, sondern meist erst bei Verkettung unglücklicher Umstände.

    Wie reagiert der Betreiber des Programmes? Er kann nicht Details einsehen. Er wird den Fehler makroskopisch protokollieren und dann mittels Reset oder anderen ihm gewohnten Verhaltensweisen die Software wieder in die gewohnten Bahnen bringen. Wenn der Fehler dann nochmals auftritt, kann makroskopisch gegebenenfalls feststellt werden, dass die Umstände ähnlich sind. Vom Ersteller der Software wird aber möglichst schnelle Behandlung des Fehlers gefordert, mindestens eine plausiblie Erklärung. Angenommen, das Problem passiert bei verschiedenen Kunden in der Welt verstreut. Direkt in der Originalumgebung nachstellen, möglichst mit einem Debugger beobachten - nicht möglich. Aber der Fehler muss erklärt und behoben sein, binnen wenigen Tagen!

    Mit dem Stacktrace wie er aus Java bekannt ist hat man sehr genau die Stelle im Quellprogramm, an der die Ausnahme passiert ist. Zugehörige Daten liefert der Stacktrace auch in Java nicht. Das wäre zuviel des erwartbaren, weil die Daten naturgemäß vielfältig sind:

    Diese Informationsmenge ist nicht verwaltbar im Stacktrace. Aber man hat meist durch Traces, Logs, Fehlermeldeprotokolle, die sowieso Bestandteil des Anwender-Bedien- und Beobachtungs-Systems sind, Informationen über die Umgebungsbedingungen.

    Aus diesen beiden Informationen sollte es meist gelingen, die Ursache des Falles zu ergründen.

    Ohne den Stacktrace wüßte man aus einem "blue screen shot" gegebenenfalls die Fehlerstelle aus maschinentechnischer Sicht. Welche Subroutine das ist, müsste man erst über Listings ergrüunden. Von wo der Aufruf passiert ist ist nur durch Hex-Dump-Analyse des Stacks (falls vorhanden) möglich. Mit dem Stacktrace hat man jedoch auf einfache Weise klare Informationen:

    Damit soll die Wichtigkeit des Stacktraces betont werden. Die Frage ist, welcher Aufwand steht in C/C++-Umgebungen dahinter?

    Im Stacktrace stehen pro Aufrufebene nur wenige konstante Informationen

    Diese konstanten Informationen sind in 4 Speicherworten darstellbar, in einer Struktur zusammengefasst. Möglich ist es, diese im Stack selbst zu speichern. Dazu muss lediglich eine Strukturvariable lokal in jeder Subroutine angelegt werden. Die Initialisierung ist ein einfaches Laden von Konstanten auf die Strukturelemente. Das geht relativ einfach.

     void anyRoutine(args)
     { Stacktrace stacktrace = { previous, { "routineRoot", __FILE__, __LINE}};
       ....
    

    Eine andere Möglichkeit wäre das Stapeln von Stacktrace-Einträgen auf einem separatem Speicherplatz. Dann würde man nur drei Einträge pro call-Level haben. Stattdessen muss ein Index auf den aktuellen Eintrag global geführt werden. Diese Möglichkeit braucht etwas "aufwendiges globales" und ist erstmal nicht in Erwägung gezogen. Ggf. ist es auch zeitaufwendiger im nicht-Fehlerfall.

    Das einzige Problem beim Stacktraceeintrag im Stack stellt die Verwaltung des Pointers previous auf den vorigen Stacktraceeintrag dar.


    3.1 Bilden einer verketten Liste von Stacktrace-Einträgen

    Topic:.Exception_Jc.StacktrcCpp.StacktrcCppPrev.


    Um den Stacktrace rückwärts verfolgen zu können, muss vom aktuellen Eintrag aus jeweils zum davorliegenden verzeigert werden. Die Frage ist, wie ist dies einfach zu organisieren. Dabei gelten folgende Premissen:

    Der Stacktrace soll den normalen Programmfluss zeitlich, speichermäßig und programmieraufwandmäßig nicht stark belasten. Zeitlich und speichermäßig versteht sich daraus, dass C mehr noch als C++ dort verwendet wird, wo es um harte Echtzeit geht oder wo weniger leistungsfähige (preiswerte) Prozessoren viel Arbeit leisten müssen. Der Programmieraufwand wird dagegen an zwei wichtigen Faktoren gemessen:

    Im folgenden werden einige Möglichkeiten diskutiert, um die in CRuntimeJavalike gewählte (Threadkontext) zu begründen:

    Pointer auf davorliegenden Stacktraceeintrag automatisch bildbar?

    Üblicherweise gibt es in jeder Ablaufumgebung einen Pointer im Stack der aktuellen Ebene, die auf die vorige Ebene zeigt. Bei Intel-Prozessoren ist das der gepushte Wert des BP-Registers (Base Pointer), auf den sich die Zugriffe auf Stackvariable beziehen. Der Inhalt des BP-Registers wird implizit bei jedem Subroutinenaufruf übergeben. Wenn es gelingen könnte, Stacktraceeinträge in einem konstanten Offset zur Speicheradresse, die vom BP gezeigert wird, unterzubringen, dann kann man auch den Pointer auf den davorliegenden Stackeintrag ohne explizite Angabe beim Programmieren bilden. Das gelingt aber nicht, da das Verhalten der Compiler bei Adressbildungen bei Stackvariablen nicht nur unterschiedlich ist, sondern auch von diversen Optionen und Optimierungen abhängt.

    Pointer auf davorliegenden Stacktraceeintrag als Argument beim Subroutinenaufruf übergeben?

    In einem Vorentwurf zum Stacktrace_Jc wurde der previous-Zeiger immer als Subroutinenargument übergeben. Das ist laufzeitoptimal:

    void routineRoot()
    { Stacktrace stacktrace = { 0, { "routineRoot", __FILE__, __LINE}};
      routine1(args, &stacktrace);
    }
    //
    void routine1(int args, Stacktrace_Jc* stacktracePrev)
    { Stacktrace stacktrace = { stacktracePrev, { "routine1", __FILE__, __LINE}};
      ...
    }
    

    Aber: Der Pferdefuß ist, dass immer dieses Subroutinenargument stacktracePrev immer übergeben werden muss, sonst ist die Kette des Stacktrace gebrochen und nicht verwendungsfähig. Es gibt aber Programmiersituationen, bei denen dieses Subroutinenargument nicht übergeben werden kann. Beispiele dafür sind:

    Demzufolge ist dieses Konzept nicht durchhaltbar.

    Globale Variable

    Wenn ein Subroutinenargument nicht verfügbar sein kann, dann hilft generell ein Zugriff auf einen globalen Wert. Das geht aber nur, wenn nicht mehrere Threads im Spiel sind:

    Stacktrace* actStacktrace= null;
    //
    void routineRoot()
    { Stacktrace stacktrace = { actStacktrace, { "routineRoot", __FILE__, __LINE}};
      actStacktrace = &stacktrace
      routine1(args);
      actStacktrace = stacktrace->previous;
    }
    //
    void routine1(int args)
    { Stacktrace stacktrace = { actStacktrace, { "routine1", __FILE__, __LINE}};
      ...
      actStacktrace = stacktrace->previous;
    }
    

    Auffällig ist hier, dass die globale Variable am Ende der jeweiligen Subroutine immer zurückgesetzt werden muss auf den previous-Wert. Das könnte vergessen werden, mit fataler Folge: Der actStacktrace zeigert eine Position im Stack, die nicht mehr gültig ist. Das ist in C++ aber einfacher lösbar indem diese Operation im Destruktor einer Stacktrace_Jcpp-class realisiert ist. Der Destruktur wird immer gerufen beim Verlassen des Stack-Kontextes.

    Das Problem ist hier das Multithreading. Deshalb geht das nicht so.

    Klassenvariable

    Klassenvariable hört sich moderner an als Globale Variable und löst woanders auch alle Probleme? Aber auch bei Klassen-Methoden gibt es Multithreading! Der Stacktrace ist keine Funktionalität einer Klasse, sondern der Systemablaufkontrolle. Würde man eine Klassenvariable stacktracePrev einführen, kommt man sofort in Schwierigkeiten wenn Methoden dieser Klasse mit der gleicher Instanz in verschiedenen Threads verwendet werden. Bei Strings kann das sehr typisch sein, aber auch sonst. Die Aussage, eine Klasseninstanz sei immer einem Thread zuordenbar (UML: Aktive Klasse) ist praktisch nicht haltbar. Also: geht auch nicht - keine systematische Lösung.

    Nutzung eines Threadkontext

    Die Stacktrace-Problematik ist eine systemglobale Sache, nicht Sache der Anwenderprogrammierung. Aber sie ist threadgebunden! Das ist die entscheidende Aussage. Für einen Thread gilt die eine globale Zugriffsmöglichkeit - in einem anderen Thread muss woanders global zugegriffen werden. Dann kommen die Stacktraces nicht durcheinander. Die Lösung ähnelt der mit globaler Variable:

    void routineRoot()
    { Stacktrace stacktrace = { threadContext->stacktrace, { "routineRoot", __FILE__, __LINE}};
      threadContext->stacktrace = &stacktrace
      routine1(args);
      threadContext->stacktrace = stacktrace->previous;
    }
    //
    void routine1(int args)
    { Stacktrace stacktrace = { threadContext->stacktrace, { "routine1", __FILE__, __LINE}};
      ...
      threadContext->stacktrace = stacktrace->previous;
    }
    

    Die Threadkontext-Lösung ist in CRuntimeJavalike benutzt. Von Bedeutung hierbei ist, wie ist der Zugriff zum Threadkontext möglich. Dazu gibt es drei generelle Möglichkeiten:

  • Der Zeiger threadContext auf den betreffenden Threadkontext wird bei jedem Subroutinenaufruf als Argument übergeben. Die Kette der Subroutinenaufrufe ist der betreffende Thread, die Weitergabe der Referenz beim Aufruf wäre also die passende Variante.

  • Es gibt eine globale Variable im System, die den Zeiger auf den aktuellen ThreadContext enthält. Diese globale Variable muss aber beim Threadwechsel mit behandelt werden, kann also nur ein Bestandteil des Threadsystems selbst sein. Die globale Variabel zählt damit zum CPU-Kontext.

  • Der Zeiger wird über eine globale Routine abgefragt. Diese Routine muss mit dem Threadsystem zusammenarbeiten.

  • Die erste Variante hat den selben Pferdefuß wie die Übergabe des stacktracePrev als Subroutinenargument: Es lässt sich nicht immer durchhalten. Ansonsten ist es eine ressourcenoptimale Variante. Demzufolge: Immer wo angebracht sollte dieses Subroutinenargument verwendet werden.

    Die zweite Variange ist gegebenenfalls noch günstiger bezüglich der Rechenzeitbilanz als die erste. Das Problem daran ist, dass das Multithreadsystem dieses unterstützen muss. Die globale Variable muss auch versorgt werden, wenn ein Threadwechsel aufgrund einer Verdrängung stattfindet. Bei einem wenig leistungsfähigen Prozessor wird man meist ein selbst optimiertes Multithread-Betriebssystem haben, da kann man diese Variante vorsehen. Bei einem fremdgenutzten System, auch beispielsweise bei der Simulation des Systems unter Windows, hat man das nicht. Aber in diesen Fällen sind meist leistungsfähige CPUs am Werk, so dass sich die dritte Variante nutzen lässt.

    Die dritte Variante ist immer realisierbar, auch bei einem ansonsten nicht beeinflußbaren Threadsystem. Die Frage ist hier: Wieviel Rechenzeit kostet die Abfrage.

    Damit ist die entscheidende Frage: Auswahl Variante 2 oder 3, ohne die Anwenderprogrammierung ändern zu müssen. Schlüssel dafür ist ein Makro

    DEF_threadContext_Jc
    

    Dieses Makro ist im plattformspezifischen Headerfile OsSpecifica_Jc.h zu definieren, für die Windows-Platform liegt in der CRuntimeJavalike ein solcher File vor. Je nach dem eingesetztem Multithread-System kann es hier verschiedene Definitionen geben. Zielvorgabe für das Makro ist, dass eine Variable mit dem Namen threadContext vorliegen muss. Es kann sein, dass es genau solch eine globale Variable gibt, das wäre die obige Variante zwei. Dann ist das Makro leer. Wenn Variante zwei nicht realisiert werden kann, dann sollte das Makro wie in der Windows-Variante folgendes expandieren:

     ThreadContext_Jc* threadContext = getCurrent_ThreadContext_Jc();
    

    Damit ist eine Stackvariable des geforderten Namens vorhanden, die initialisiert ist. Für die obige erste Variante gibt es dann folgende Konstellationen:

    Damit ist die Anwenderprogrammierung insgesamt unabhängig vom Laufzeitsystem.

    Zugriff auf Stacktrace von außen über ThreadContext

    Die Lösung mit dem ThreadContext hat noch einen wichtigen Vorteil: Man kann aus einem anderen Thread heraus, insbesondere zum Zwecke der Diagnose und Debug, den Stacktrace jedes Threads lesen. Voraussetzung für eine ordentliche Darstellung ist, dass dieser andere Thread steht. Genau das ist aber der Fall in Fehlerfällen (Eigenschleife, Deadlock, Suspend wegen fatalem Fehler). Über den im Threadcontext abgespeicherten Zeiger auf den Stacktrace kommt man direkt auf den Stacktraceeintrag der letzten Ebene, von dort weiter auf alle unteren Ebenen. Damit lässt sich auf einfache Weise ein Protokoll erstellen, wo dieser Thread steht und wie er dorthin gekommen ist. Die häufig anzutreffende Alternative ist eine Anzeige auf Register- und Speicherniveau: Letzter Maschinenbefehl vor Thread-Abgabe. Man kommt zwar über diesen Weg an alle Daten, muss aber umfangreich hexa blättern, Map-Files zur Hand haben und insbesondere Vor-ort bei aufgetretenem Fehler anwesend sein.


    3.2 Zeilennummern

    Topic:.Exception_Jc.StacktrcCpp.LineNr.

    Die Zeilennummer soll die Zeile angeben, an der jeweils der Aufruf der Routine erfolgt, die dann im Stacktrace in der folgenden Ebene gefunden wird. Ohne Zusatzmaßnahmen wird erstmal die Zeilennummer der Zeile der Stacktrace-Vereinbarung gespeichert. Vor jedem Aufruf einer Routine sollte die aktuelle Zeilennummer eingetragen werden. Das ist eine Anweisung mit einer einfachen Konstanenzuweisung an eine Variable (bzw. ein Strukturelement) an einer festen Position im Stack. Dazu ist auch keine Adressrechnung zur Runtime erforderlich, das wird zur Compiletime erledigt.

    stacktrace.entry.line = __LINE__;
    

    Um es beim Schreiben des Programmes einfacher zu haben, gibt es ein Makro dafür, das sich günstig in die selbe Zeile schreiben lässt wie der Aufruf der Subroutine:

    CALLINE; subroutine(args);
    

    Wird das Speichern der Zeilennummer vergessen, dann erscheint im Stacktrace die letzte Position, an der eine Zeilennummer gespeichert wurde. Das ist im Quelltext nachvollziehbar und meist verträglich.


    3.3 Überspringen einiger Levels der call-Kette

    Topic:.Exception_Jc.StacktrcCpp.NotAllLevel.

    Unter der Maßgabe, dass der Stacktrace nicht als Subroutinenargument übergeben wird, kann der Stacktrace auch in einigen Routinen schlichtweg weggelassen werden. Die Folge ist zwar, dass diese Routinen in einer Stacktrace-Ausgabe fehlen. Handelt es sich jedoch um einfache Wrapper-Routinen (Umhüllungen), dann führt deren Fehlen letzlich zur einfacheren Interpretation weil unwichtige Informationen weggelassen werden. Dazu ein Prinzip-Beispiel:

    void routine1()
    { DEF_threadContext_Jc
      Stacktrace_Jc stacktrace = { threadContext->stacktrace, { "routineRoot", __FILE__, __LINE}};
      threadContext->stacktrace = &stacktrace
      CALLINE; routine2(args);
      threadContext->stacktrace = stacktrace->previous;
    }
    //
    void routine2(int args)
    { dosomeelse();
      routine3(args);
    }
    //
    void routine3(int args)
    { DEF_threadContext_Jc
      Stacktrace_Jc stacktrace = { threadContext->stacktrace, { "routine3", __FILE__, __LINE}};
      ...
      threadContext->stacktrace = stacktrace->previous;
    }
    

    Passiert in der routine3 eine Ausnahme, dann wird im Stacktrace aufgeführt, dass routine3 aus routine1 aufgerufen wurde, und zwar an der bezeichneten Zeile, an der routine2 steht. Wenn routine2 kurz und überschaubar ist, beispielsweise ein Wrapper, dann ist bei der Fehleranalyse sofort ersichtlich, dass von dort aus routine3 gerufen wurde. Es sollten in routine2 dann nicht mehrere Programmstellen geben, an denen routine3 gerufen wird.


    4 CRuntimeJavalike

    4.1 Realisierung der Fehlerbehandlung in C/C++ java-like

    Topic:.Exception_Jc.ErrorhandlingInCRuntimeJavalike.

    Im folgenden wird der konkrete Einsatz eines Fehlerbehandlungskonzeptes mit try-catch-throw und Stacktrace in C und C++ innerhalb der CRuntimeJavalike mit einem Einsatzbeispiel gezeigt:

    #include "Exception_Jc.h"
    

    Dieses File muss includiert werden, darin wird OsSpecifica_Jc.h includiert. Damit sind die Makros, Strukturen und Funktionsprototypen der Fehlerbehandlung bekannt.

    Der Stacktrace wird gleich in der main-Routine berücksichtigt. Für die Anlage des Stacktrace-Elementes wird ein Makro benutzt. Das Makro STACKTRC_ENTRY berücksichtigt gleichzeitig Anlage und Initialisieren einer Stackvariablen threadContext. Auf den Threadcontext muss für den Stacktrace zugegriffen werden, um eine verkettete Liste zum jeweils vorigen Eintrag zu bilden. Hinweis: Das STACKTRC_ENTRY-Makro muss in C als letztes bei Variablendefinitionen stehen, da es selbst eine Folge von Vereinbarung und Zuweisung enthält.

     int main(int argc, char** argv)
     { STACKTRC_ENTRY("main");
    

    Die main-Routine läuft in dem vom System angelegten main-Thread. Hier erfolgt auch gleich die Benennung des main-Thread im threadContext und die Anlage eines Puffers für StacktraceEntry-Einträge für die Try-Behandlung. Letzteres ist unumgänglich. Hier werden für max.200 Ebenen zwischen try und throw berücksichtigt. Das ist ein Schätzwert. Die Begrenzung ist nur wirksam für die Anzeige des Stacktrace, nicht für die Kern-Funktionalität der Fehlerbehandlung. Da der Speicher im Heap angelegt wird, ist auch ein wesentlich höherer Wert verträglich:

       threadContext->name="mainThread";
       threadContext->stacktraceBuffer = new_StacktraceEntry_JcARRAY(200);
    

    Im weiteren Verlauf wird eine Routine gerufen, die eine Exception werfen könnte. Daher wird hier mit dem Makro CALLINE die aktuelle Zeilennummer im stacktrace eingetragen.

       CALLINE; testException();
    

    Nach weiteren Anweisungen wird die main-Routine beendet:

       STACKTRC_LEAVE;
     }
    

    Die Subroutine testException beginnt ebenfalls mit:

     void testException()
     { int a;
       STACKTRC_ENTRY("testException")
       a = 6;   //testvalue, vary it!
    

    In dieser Routine ist ein try-Block vorhanden. Folgend wird eine vollständige Beispielstuktur gezeigt:

       TRY
       { CALLINE; testExceptionThrow(a, threadContext);
       }_TRY
       CATCH(IndexOutOfBoundsException, exc)
       { printf("Test_Exception, a=%i, =>IndexOutOfBoundsExceptionJc\n", a);
         printStackTrace_Exception_Jc(exc, stdout);
       }
       CATCH(IllegalArgumentException, exc)
       { printStackTrace_Exception_Jc(exc, stdout);
       }
       FINALLY
       {
         printf("finally, call everytime in try block.\n");
       }
       END_TRY
    

    Zu beachten ist der Abschluss des eigentlichen try-Blockes mit _TRY_jc. Danach beginnt der erste von ggf. mehreren catch-Blöcken. Damit lassen sich wie bei Java mehrere Exceptions oder Exceptiongruppen (in Java die Superclasses von Exceptions) abfangen. exc ist wie in Java das Exception-Object. Es wird aber hier im Stack angelegt und lebt von dem Stacktrace nach weiter vorn aus der aktuellen Stackebene und von Einträgen im threadContext. Damit steht es bestens geeignet für die hier gezeigte Verwendung in einer Subroutine printStackTrace..() Wenn man aber das Exception-Object weitergeben will, beispielsweise mit einem Event an einen anderen Thread verschicken, weil dieser die Anzeige besorgen soll, dann muss man den Inhalt gesichert kopieren. Das betrifft den Stacktrace.

    In jedem Fall, mit oder ohne Exception, wird der FINALLY-Block durchlaufen, ebenfalls wie in Java. Im Unterschied zu Java wird der FINALLY-Block aber nicht durchlaufen, wenn im im CATCH die Subroutine beendet wird. Der FINALLY-Block hat den Zweck, das er auch durchlaufen wird wenn die Exception nicht abgefangen sondern weitergereicht wird.

    Wenn eine Exception nicht abgefangen wurde, dann wird im END_TRY-Makro ein erneutes THROW ausgelöst, dass gegebenenfalls weiter vorn in der Aufruffolge abgefangen wird. Dann wird die Subroutine an diesem Punkt verlassen. Das Ende der Beispiel-Subroutine sei noch vollständigkeitshalber dargestellt:

       STACKTRC_LEAVE
     }
    

    Die Anweisung STACKTRC_LEAVE muss vor dem return stehen und darf nicht vergessen werden. Die gerufene Routine wird im folgenden komplett dargestellt:

     void testExceptionThrow(int a, ThreadContext_Jc* threadContext)
     { STACKTRC_TENTRY("testExceptionThrow")
       if(a < 0)
       { THROW(IllegalArgumentException_Jc, "description", a);
       }
       else if(a >= 5)
       { THROW(IndexOutOfBoundsException_Jc, "description", a);
       }
       else
       { printf("Test_Exception, a=%i, ok\n", a);
       }
       STACKTRC_LEAVE
     }
    

    Diese Subroutine übernimmt den threadContext als Argument des Aufrufes, demzufolge steht hier STACKTRC_TENTRY(...) ohne X. In der Beispielroutine wird a getestet und je nachdem Exceptions ausgelöst. THROW führt in C++ zu einem tatsächlichen throw. Für C wird hier der longjmp-Mechanismus verwendet. Die Wirkung ist in etwa gleich. Ein wichtiger Unterschied ist aber folgender: throw sollte in C++ beachten, dass in Zwischenebenen Destruktoren von derjenigen Objekten aufgerufen werden, die dort im Stack angelegt worden sind. In C gibt es keine Destruktoren, longjmp überspringt alle Call-Stackebenen bis zu der Stelle, wo der longjmp-Buffer eingerichtet ist, das ist beim TRY. Wenn in einer Zwischenebene kein Fehler abgefangen werden soll, aber es muss eine Abspannmaßnahme getroffen werden, etwas im Speicher freigegeben, ein File geschlossen oder ähnliches, dann kann in dieser Zwischenebene geschrieben werden:

     void mediLevel()
     { STACKTRC_ENTRY("mediLevel")
    
       TRY
       { doSomenting();
       }_TRY
       FINALLY
       { shutdownSomething();
       }
       END_TRY
       STACKTRC_LEAVE
     }
    

    Es gibt also nur den TRY- und FINALLY-Block. FINALLY wird auf jeden fall erreicht, wenn irgendwo tiefer im doSomething() ein THROW geworfen wird. Danach wird von dieser Routine das THROW weiter nach oben geworfen.

    Bei Abarbeitung dieses Beispiels entsteht als Bildschirmausgabe

     IllegalArgumentException: description: -1
       at testExceptionThrow (d:\vishia\jc\test_msc6\src\test_exception.c:69)
       at testException (d:\vishia\jc\test_msc6\src\test_exception.c:47)
       at main (d:\vishia\jc\test_msc6\src\test_main.c:43)
     finally, call everytime in try block.
    

    Bei ausschließlicher Verwendung in C++ kann man den Stacktrace wie folgt organisieren:

     void Myclass::Example()
     { DEF_threadContext_Jc
       Stacktrace_Jcpp stacktrace("Myclass::Example", threadContext);
       ...
     }
    

    Man braucht also kein STACKTRC_LEAVE, statt dem STACKTRC_TENTRY legt man den stacktrace über Konstruktor an. Das Makro DEF_threadContext_Jc dient wie in C zur Anlage und Intialisierung eines Zeigers threadContext, der aber wie in C alternativ auch als Methodenargument übergeben werden kann. Schreibt man

     void Myclass::Example()
     { Stacktrace_Jcpp stacktrace("Myclass::Example"));
       ...
     }
    

    dann wird der ThreadContext implizit im Konstruktor von Stacktrace_Jcpp ermittelt. Das ist also noch einfacher zu programmieren und dann zu empfehlen, wenn man die Varibale threadcontext ansonsten in der Methode nicht braucht.


    4.2 Blick hinter die Kulissen

    Topic:.Exception_Jc.ErrorhandlingInCRuntimeJavalikeDetail.

    Folgend ist das Anwendungsbeispiel aus CRuntimeJavalike genauer auseinandergenommen um darzustellen was dabei passiert. Dabei wird sowohl die C++-Variante mit richtigem throw und die C-Variante mit dem longjmp betrachtet. Es werden die selben Beispiel-Codes wiederholt. Das Beispiel - der Usercode - ist in einer anderen Hintergrundfärbung wie erklärende C-Codes dargestellt. Man sollte also das Beispiel optisch von oben nach unten verfolgen auch wenn es sehr zerstückelt ist.


    4.2.1 main, stacktrace, ThreadContext-Initialisierung

    Topic:.Exception_Jc.ErrorhandlingInCRuntimeJavalikeDetail..

    Das Beispiel beginnt wie folgt:

     #include "Exception_Jc.h"
    
     int main(int argc, char** argv)
     {
    

    Das folgende

       STACKTRC_ENTRY("main")
    

    - Makro wird expandiert nach

     DEF_threadContext_Jc
     Stacktrace_Jc stacktrace = {threadContext->stacktrace, { NAME, __FILE__, __LINE__} , (threadContext->stacktrace = &stacktrace, null)};
     { threadContext->stacktrace = &stacktrace; }
    

    Die Definition des enthaltenen Makros DEF_threadContext_Jc hängt vom Multithread-Betriebssystem ab. Im Idealfall ist es ein leeres Makro, weil das Betriebssystem eine globale Variable threadContext definiert und richtig bei Threadumschaltungen versorgt. Das ist für Systeme mit langsamen Prozessoren in schneller Echtzeit jedenfalls zu empfehlen. Im Fall von Windows verbirgt sich dahinter die Zeile

     ThreadContext_Jc* threadContext = getCurrent_ThreadContext_Jc();
    

    Diese ist insoweit etwas schwergewichtiger, weil in der gerufenen Subroutine zunächst über die Windows-API-Funktion GetCurrentThreadId() die Ident des aktuellen Threads ermittelt wird. Das dauert nach Angaben im Internet (TODO: link) 27 bis 71 Takte, das sind wenige 10 ns. Dieses Aussage ist durch eigene Rechenzeitmessungen verifiziert.

    Nach Ermittlung der ThreadId muss noch die ThreadId in einer Tabelle (Map list) gesucht werden, weil die ThreadId irgendwelche Zufalls-Nummern sind, sich also nicht für eine direkte Indizierung eignen. Das wird mit binärer Suche realisiert (sortiert), und dauert ca. je nach Anzahl Threads in der Liste nochmal einige 10 ns. Diese Zeiten dürften aber immernoch uninteressant für PC-Anwendungen sein. Ungünstig ist es allerdings, das STACKTRC_ENTRY() für Subroutinen einzusetzen, die in einer Schleife eine Millionen mal gerufen werden. Dann ist man bei einigen zig Millisekunden.

    Das Anlegen der Stacktrace-Instanz ist dagegen recht schnell, einige wenige CPU-Befehle. Der Stacktrace ist damit verkettet mit den aufrufenden Ebenen, weil im threadContext->stacktrace zuvor der stacktrace der aufrufenden Ebenen referenziert war. Wenn man in einer call-Ebene einmal nicht diesen Stacktrace einrichtet, dann fehlt diese Ebene dann später in der Auswertung. Schlimmere Nebeneffekte gibt es nicht. Daher sollte man sich für einfache wrapper-Subroutinen den Stacktrace auch sparen.

    Die weiteren Anweisungen sind fast selbsterklärend. Das new_StacktraceEntry_JcARRAY() findet man definiert in Exception_Jc.h, und zwar als einfaches Wrapper-Makro basierend auf dem Aufruf von new_ObjectArray_Jc() definiert in Object_Jc.h. Das ist eine Subroutine, die Speicherplatz anfordert:

       threadContext->name="mainThread";
       threadContext->stacktraceBuffer = new_StacktraceEntry_JcARRAY(200);
    

    Das folgende CALLINE-Makro enthält nur eine Anweisung (stacktrace.entry.line=__LINE__). Das dabei verwendete __LINE__ ist ein Makro aus der C-Standarddefinition:

       CALLINE; testException();
    

    Das STACKTRC_LEAVE ist katastrophal wichtig. Wird es vergessen, dan wird im threadContext ein stacktrace-Objekt gezeigert, dass nicht mehr im Stack steht. Das wird dann noch in die Verkettung aufgenommen, und die Verkettung ist futsch. Benutzt man konsequent C++, dann gibt es die Möglichkeit, das Makro einzusparen (damit kann es nicht vergessen werden), in dem man den automatisch gerufenen Destruktor des Stacktrace_Jcpp dafür nutzt. Die Beispiele hier sind zunächst aus der C-Sicht beschrieben. Vom Programmieren her gilt in etwa folgende Faustregel:

    Insoweit ist die Notwendigkeit des aufpassens auf das folgende Makro etwas entschärft.

       STACKTRC_LEAVE;
     }
    

    4.2.2 try

    Topic:.Exception_Jc.ErrorhandlingInCRuntimeJavalikeDetail..

    Die Subroutine testException beginnt ebenfalls mit eine STACKTRC_ENTRY:

     void testException()
     { int a;
       STACKTRC_ENTRY("testException")
       a = 6;   //testvalue, vary it!
    

    Jetzt wird es interessant. Beim

       TRY
    

    gibt es die Unterschiede der Implementierung für C und C++. Das tryObject ist dasjenige, was zunächst die Exceptiondaten aufnimmt. Es ist wie folgt definiert, der longjmpBuffer ist nur für die C-Variante enthalten:

     typedef struct TryObject_Jc_t
     {
       #ifndef __TRYCPP_Jc
         jmp_buf longjmpBuffer;
       #endif
       int32 exceptionNr;
       String_Jc exceptionMsg;
       int exceptionValue;
     }TryObject_Jc;
    

    In C wird das Makro TRY wie folgt untersetzt:

     { TryObject_Jc tryObject = {0};
       stacktrace.tryObject = &tryObject;
       stacktrace.entry.line = __LINE__;
       { tryObject.exceptionNr = setjmp(tryObject.longjmpBuffer);
         if(tryObject.exceptionNr==0)
         {
    

    Damit entstehen hier insgesamt drei öffnende Anweisungsklammern, die teils mit dem CATCH geschlossen werden, teils erst ganz hinten mit dem END_TRY. Das tryObject wird im stacktrace verankert, wesentlich für ein Throw_Jc. Die Anweisung stacktrace.entry.line = __LINE__; erfolgt hier, falls vor dem betreffendem Subroutinen-call kein CALLINE steht. Damit erscheint mindestens die TRY-Zeile in der Stacktrace-Ausgabe.

    Die Abfrage if(tryObject.exceptionNr==0) ist mit dem setjmp() im Zusammenhang zu betrachten. Beim direkten Aufruf, also im linerarem Ablauf, liefert setjmp() =0. Wenn setjmp() aufgrund longjmp() des throw zurückkehrt, ist der Wert ungleich 0.

    Die C++-Variante für TRY sieht ähnlich aus, Unterschied ist nur die letzte Zeile:

     { TryObject_Jc tryObject = {0};
       stacktrace.tryObject = &tryObject;
       stacktrace.entry.line = __LINE__;
       tryObject.exceptionNr = 0;
       try
    

    Hier wird das originale C++-try gerufen.

    Im Beispiel folgt der Aufruf einer Subroutine mit CALLINE, wie oben bereits erläutert.

       { CALLINE; testExceptionThrow(a, threadContext);
    

    4.2.3 catch

    Topic:.Exception_Jc.ErrorhandlingInCRuntimeJavalikeDetail..

    Interessant ist das den eigentlichen TRY-Block abschließende

       }_TRY
    

    Das _TRY wird bei C expandiert nach

     stacktrace.tryObject = null;
    

    Mehr nicht. Diese Anweisung verhindert, dass bei einem Aufruf von THROW außerhalb dieses TRY-Blockes ein nicht mehr vorhandenes tryObject verwendet wird.

    Bei C++ ist dieses Makro dagegen umfangreicher:

     catch(...)
     { stacktrace.tryObject = null;
       if(false)
       {
    

    Hier liefert das _TRY die öffnenden Klammern, die in der C-Variante beim TRY stehen. Der Code danach ist also wieder klammermäßig identisch.

    Das catch(...) ist hier ein unspezifiziertes catch. In C++ können mit einem catch Ausnahmeobjekte übergeben werden, anhand der Typen der Ausnahmeobjekte kann eine Fallunterscheidung erfolgen, ähnlich wie in Java. Das wird hier nicht genutzt, mit folgender Begründung:

    Hier ist der Gesamtansatz zu betrachten. Da sowieso der Stacktrace mit in der Ausnahmebehandlung integriert ist, liegt es nahe, auf ein Ausnahmeobjekt beim throw -> catch zu verzichten und statt dessen das tryObject zu verwenden. Dieses wird auf der try-Ebene angelegt. Wegen der Stacktraceproblematik muss beim throw sowieso diese Ebene aufgesucht werden, Dann kann man dort gleich auch die Daten der Ausnahme ablegen.

    Nebeneffekt: Jedes throw kommt beim catch(...) an, auch ein throw aufgrund MemoryException oder ähnliches in Systemroutinen. Das ist aber systemabhängig. Bei Windows ist es so.

    Die if(false)-Anweisung ist nur syntaktisch nötig.

    Das nachfolgende

       CATCH(IndexOutOfBoundsException, exc)
    

    ist also gar kein catch aus C++-Sicht, sondern wird in C und C++ gleicherweise aufgelöst als

       } else if((tryObject.exceptionNr & mask_IndexOutOfBoundsException_Jc)!= 0)
       { Exception_Jc* exc = &tryObject.exc;
         tryObject.exceptionNr = 0;  /*do not check it a second time.*/
    

    Die Erkennung des catch-Falles erfolgt also unter Abfrage der exceptionNr. Da mehrere Exception-Typen zu einer Gesamtabfrage zusammengefasst werden können (wie in Java bei Benutzung von Super-Exception-classes), erfolgt die Erkennung über mehrere Bits als Maskenabfrage. Die schließende Klammer am Anfang passt zur öffnenden Klammer der vorigen CATCH-Zeile. Ist dies die erste CATCH-Zeile, dann kann das hier nicht unterschieden werden, ein extra Makro CATCHfirst_Jc macht die Sache für den Anwender zu komplex. Daher wird das syntaktisch notwendige if(...){ vom _TRY geliefert. Das if(false) wird letzlich von der Compileroptimierung vollkommen aufgelöst, erzeugt also keinen toten Code auf Maschinenebene. Eine etwaige Warning (compiler- und option-spezifisch) sollte man unterdrücken.

    Die Referenz auf das Ausnahmeobjekt wird in der angegebenen Variable gespeichert. Damit ist das einerseits syntaktisch Java-ähnlich, andererseits wäre der Zugriff auf das Ausnahmeobjektes mitten in der TryObject_Jc-Struktur syntaktisch unangemessen komplex für den Anwender. Man muss aber beachten, dass das Ausnahmeobjekt im Stack der aktuellen Ebene steht. Man darf also nicht wie in Java die Referenz einfach weitergeben sondern nur unmittelbar als Argument von Methoden, die hier im selben Thread ausgeführt werden, verwenden. Es gibt eine manifest_Exception_Jc()-Methode, die das sichere Kopieren erledigt.

    Als letzter Befehl wird die exceptionNr im tryObject auf 0 zurückgesetzt. An sich könnte das gesamte tryObject mit 0 belegt werden. Dieser Befehl ist wichtig, weil im END_TRY nicht abgefangene Exceptions erkannt werden, aufgrund des noch gesetzten excetionNr.

    Im Beispiel folgt der Anweisungsblock zum catch. Hier soll für die Auswertung das Exception-Object benutzt werden. Die printStackTrace_Exception_Jc()-Methode ist Java nachempfunden. Hier genügt die Referenz auf das Ausnahmeobjekt im Stack, da die Abarbeitung im selben Thread bleibt:

         { printf("Test_Exception, a=%i, =>IndexOutOfBoundsException\n", a);
           printStackTrace_Exception_Jc(exc, stdout);
         }
    

    Würde man aber statt dessen die Information in einen anderen Thread übergeben, dann muss auf Anwenderebene eine passende Subroutine gerufen werden wie beispielsweise im folgenden catch-Fall:

         CATCH(IllegalArgumentException_Jc, exc)
         { sendException(exc, dstThread);
         }
    

    Diese Routine kann die Daten der Ausnahme in eine andere Struktur packen, die gegebenenfalls in einer Liste aufgehangen ist. Dabei soll das oben erwähnte manifest_Exception_Jc() verwendet werden. Dabei werden auch die Informationen des Stacktrace komplett kopiert, so dass nichts aus dem aktuellen Thread bzw. Stack benutzt werden muss. Das weiter folgende


    4.2.4 finally

    Topic:.Exception_Jc.ErrorhandlingInCRuntimeJavalikeDetail..

    Das nun folgende

         FINALLY
    

    wird wie aufgelöst nur in eine Folge von

       }
     }
     {
       {
    

    Damit wird der letzte CATCH-if-Block geschlossen, danach der gesamte catch-Block bzw. im TRY-Block in C die öffnende Klammer vor dem setjmp(), Damit sind wir abarbeitungstechnisch hinter den else if der catch-Abfragen und hinter dem try-Block, diese Anweisungen werden also immer durchlaufen, auch im Fehlerfall:

         {
           printf("finally, call everytime in try block.\n");
         }
    

    4.2.5 END_TRY, nicht behandelte Ausnahmen

    Topic:.Exception_Jc.ErrorhandlingInCRuntimeJavalikeDetail..

    Das darauf folgende

         END_TRY
    

    ist nun wieder etwas umfangreicher expandiert zu:

       } /*close FINALLY, CATCH or TRY brace */
     } /*close brace of whole catch block*/
     if(tryObject.exceptionNr != 0) /*instead else of CATCH blocks, notice, FINALLY may be assigned after CATCH-Blocks. */\
     { /* delegate exception to previous level*/ \
       stacktrace.tryObject = null; /*Do not use the own longjmp!!! */ \
       throw_Jc(tryObject.exceptionNr, &tryObject.exceptionMsg, tryObject.exceptionValue, threadContext, __LINE__); \
     } \
    } /*close brace from beginning TRY*/
    

    Hier wird also ein throw ausgelöst dass in einer Ebene davor gegebenenfalls abgefangen werden kann, wenn immer noch eine exceptionNr gespeichert ist, also kein catch-Block durchlaufen wurde.

    Das im Beispiel folgende:

       STACKTRC_LEAVE
     }
    

    ist umgesetzt nach

       threadContext->stacktrace = stacktrace.previous;
     }
    

    dient also dem Aufräumen. An dieser Stelle sei angemerkt, dass für C++ ein Stacktrace_Jcpp("name", threadContext) angelegt werden kann. Diese Klasse hat den hier gezeigten Konstruktur, der Destruktur wird automatisch aufgerufen. Damit ist das STACKTRC_LEAVE bei ausschließlicher C++-Compilierung nicht nötig.


    4.2.6 throw

    Topic:.Exception_Jc.ErrorhandlingInCRuntimeJavalikeDetail..

    Die im Beipsiel gerufen Subroutine beginnt wieder mit

     void testExceptionThrow(int a, ThreadContext_Jc* threadContext)
     { STACKTRC_TENTRY("testExceptionThrow")
    

    Da hier der threadContext als Argument übergeben wird, wird statt STACKTRC_ENTRY die Variante ohne X (nicht eXtended) gerufen.

    Folgend wird beispielhaft eine Bedingung geprüft und ein THROW ausgelöst:

       if(a < 0)
       { THROW(IllegalArgumentException, s0_String_Jc("description"), a);
       }
    

    Das THROW wird folgendermaßen aufgelöst:

     throw_Jc(ident_IllegalArgumentException_Jc, s0_String_Jc("description"), a, threadContext, __LINE__);
    

    Es wird also eine Subroutine gerufen. Diese Subroutine muss erstmal im Stacktrace diejenige Stelle aufsuchen, an der das TRY steht. Dort werden dann im Exception_Jc des TryObject_Jc die Fehlerkennung und der Begleitwert und -text eingetragen. Der Stacktrace der Ebenen vom TRY zum THRWO wird in den Threadcontext kopiert. Das ist unumgänglich, da der Stackbereich, in dem diese Informationen stehen, danach aufgegeben wird. Das sieht nach einer ganzen Menge Arbeit aus. Das hängt natürlich an der Anzahl der Stackebenen zwischen TRY und THROW. Rechenzeitmessungen am PC haben folgende Werte ergeben: Eine Ebene braucht ca. 2 ns, das wären bei 10 Ebenen 20 ns. Das throw selbst braucht aber 3.4 Mikrosekunden. Das liegt am Compiler und Laufzeitsystem. longjmp() braucht dagegen nur 115 ns. Das sind PC-Verleichszahlen. Auf einem kleinen langsamen Prozessor hat man einige Mikrosekunden stattdessen.

    Im folgenden ist die gesamte Routine wiedergegeben:

     void throw_Jc(int32 exceptionNr, String_Jc msg, int value, ThreadContext_Jc* threadContext, int line)
     { //find stack level with try entry:
       Stacktrace_Jc* stacktrace = threadContext->stacktrace;
       StacktraceEntry_JcARRAY* buffer = threadContext->stacktraceBuffer;
       int idxBuffer = 0;
       int idxBufferMax;
       idxBufferMax = buffer == null ? -1 : buffer->head.length;
       stacktrace->entry.line = line;
       while
       (  stacktrace != null             //false only if no try-level found, if the end of the stacktrace is reached
       && stacktrace->tryObject == null  //while try-level not reached
       )
       { if(idxBuffer < idxBufferMax)
         { //fill in infos
           buffer->data[idxBuffer] = stacktrace->entry; //NOTE: it is a memcpy.
           idxBuffer += 1;
         }
         stacktrace = stacktrace->previous;
       }
       threadContext->stacktrace = stacktrace;  //may be null if no TRY_Jc-level is found.
       threadContext->nrofEntriesStacktraceBuffer = idxBuffer;
       //
       //the threadContext->stacktraceBuffer is filled with the followed levels of Stacktrace,
       //the stacktrace refers the level of the TRY or it is null.
       //
       if(stacktrace != null)
       { //TRY-level is found:
         stacktrace->tryObject->exceptionNr = stacktrace->tryObject->exc.exceptionNr = exceptionNr;
         lightCopy_String_Jc(&stacktrace->tryObject->exc.exceptionMsg, msg);
         stacktrace->tryObject->exc.exceptionValue = value;
         //throw exception, use the platform dependend variant.
       #if defined(__TRYCPP_Jc) //&& defined(__cplusplus)
        throw exceptionNr;
       #else
        longjmp(stacktrace->tryObject->longjmpBuffer, exceptionNr);
       #endif
    
       }
       else
       { //no TRY_Jc-level found,
         uncatchedException(exceptionNr, &msg, value, threadContext);
         while(true);  //STOP the execution.
       }
     }
    

    Im Beispiel ist noch ein anderer THROW-Fall eingebaut, diesmal unter Nutzung des StringBuffer_Jc im threadContext.

     else if(a >= 5)
     { setLength_StringBuffer_Jc(threadContext->excMsg, 0);
       append_s0_StringBuffer_Jc(threadContext->excMsg, "The max.expected index is ");
       append_i_StringBuffer_Jc(threadContext->excMsg, 5);
       THROW(IndexOutOfBoundsException, toString_StringBuffer_Jc(threadContext->excMsg), a);
     }
    

    Würde man das in Java unter der gleichen Maßgabe schreiben: Verwendung eines threadspezifischen festen Buffers, dann würde das wie folgt aussehen:

     if(a.length == 1)
     { ThreadContext threadContext = ThreadContext.getCurrent();
       threadContext.excMsg.setLength(0);
       threadContext.excMsg.append("test: array with 1 element, at least:").append(idx+1);
       throw new RuntimeException(threadContext.excMsg.toString());
     }
    

    Das ist ein etwas anderes Beispiel, was sich in javadoc-src:_vishia/example/common/TestTry.java findet. Auch in Java ist ein threadContext realisiert, die Klasse steht unter javadoc-src:_org/vishia/util/ThreadContext.java.


    5 Rechenzeitmessungen

    Topic:.Exception_Jc.calctimes.

    Die Rechenzeiten sind deshalb interessant, weil die Organisation eines Stacktraces die Gesamtabarbeitung auch im Gut-Fall beeinträchtigt. Nur wenn diese Zeiten fast vernachlässigbar sind, wird man sich diesem Konzept annehmen. Die Zeiten, die im Fehlerfall benötigt werden, sind dagegen nur dann von wesentlichem Interesse, wenn die Fehlerfälle nicht nur bei unerwarteten Ausnahmen sondern als Bestandteil des Algorithmus verwendet werden. Sozusagen als Alternative für eine Werterückgabe mit Abfrage.

    Die Zeiten wurden gemessen an einem Hochleitungs-PC, Prozessor x86 Family 6 Model 15 Stepping 6 GenuineIntel ~1995 Mhz. Sie sind also repräsentativ für Power-Hardware. Für kleine Prozessoren sind ns-Angaben dann fast schon Mikrosekunden :-( Grob gesehen kann man das über den Takt abschätzen. Die Compilierung erfolgte mit Visual Studio 6 mit Zeitoptimierung. Hier kann man ggf. davon ausgehen, dass ein angepasster Compiler für einen langsamen Prozessor nicht schlechter ist. Die Zeiten wurden gemessen, in dem ein Programm mit bestimmten Bedingungen mit einer hohe Anzahl von Wiederholungen eine Routine ruft, die weitere Subroutinen aufruft, die letzte löst dann gegebenenfalls ein THROW aus. Die Routinen befinden sich im download:_CRuntimeJavalike.zip im File Test_Msc6/src/Test_Exception.c Um die Zeitmessung zu aktivieren, muss man in Test_Msc6/src/Test_main.c in main() die Routine testExceptionTime() aktivieren (entkommentieren). Achtung: Nicht vergessen bei der Compilierung den Releasemodus einzustellen. Im Debugmodus werden mit ca. Faktor 15 bis 20 längere Rechenzeiten benötigt. Dieser hohe Faktor kommt dadurch zustande, weil die Routinen nicht viel machen, eigentlich nur immer rein in die Routine und wieder raus. Visual Studio produziert im Debugmodus eine Menge mehr Befehle, die die Fehlersuche unterstützen wie Initialisieren aller Stackvariable. Die reine Frage der Optimierung ist es nicht.

    Als Protokoll soll hier die printf-Ausgaben dargestellt und kommentiert werden. Die printf-Ausgaben werden jeweils nach den 1 Millionen Durchläufen als Abschluss ausgegeben. Die gemessenen Zeiten sind auf einen Durchlauf bezogen und in Nanosekunden ausgegeben:

    Zunächst wird die C-Variante dargestellt:

     d:\vishia\Jc\Test_Msc6\TestCVtblGHeap\Release>TestCVtbl_Msc6.exe
     Test_Exception, a=6, =>IndexOutOfBoundsException_Jc
    
     Exception: The max.expected index is 5: 6
     finally, call everytime in try block.
     Test Exception calculation times in C
     test normal ... calcTime = 71.900000
     test normal ... calcTime = 67.100000
     test normal ... calcTime = 65.700000
     test additional getCurrent_ThreadContext_Jc() ... calcTime = 78.200000, add = 12.500003
     add some dummy thread Contexts to increase the list of threadContextes
     test normal ... calcTime = 67.100000
     test additional getCurrent_ThreadContext_Jc() ... calcTime = 89.100000, add = 22.000002
     test without 2 Stacktrace levels ... calcTime = 53.100000, add = 13.999998
     test with throw ... calcTime = 179.699997, add = 112.600002
     test with throw without 2 Stacktrc levels... calcTime = 168.800000, add = 10.899997
    
     d:\vishia\Jc\Test_Msc6\TestCVtblGHeap\Release>TestCVtbl_Msc6.exe
     Test_Exception, a=6, =>IndexOutOfBoundsException_Jc
    
     Exception: The max.expected index is 5: 6
     finally, call everytime in try block.
     Test Exception calculation times in C
     test normal ... calcTime = 73.400000
     test normal ... calcTime = 65.600000
     test normal ... calcTime = 67.200000
     test additional getCurrent_ThreadContext_Jc() ... calcTime = 79.700000, add = 12.500003
     add some dummy thread Contexts to increase the list of threadContextes
     test normal ... calcTime = 65.600000
     test additional getCurrent_ThreadContext_Jc() ... calcTime = 90.600000, add = 25.000002
     test without 2 Stacktrace levels ... calcTime = 53.200000, add = 12.399998
     test with throw ... calcTime = 179.699997, add = 114.100002
     test with throw without 2 Stacktrc levels... calcTime = 167.100000, add = 12.599997
    

    Gezeigt sind hier 2 Durchläufe, die nacheinander manuell gestartet wurden. Die Zeiten sind etwas unterschiedlich, mehrfache Starts liegen immer etwa in den hier gezeigten Schwankungsbereich.

    Zu Anfang werden drei nomale Durchläufe ausgeführt. Dabei ist der erste Durchlauf immer etwas länger. Das wird daran liegen, dass nach dem Start der PC erstmal noch einiges in Systeminterrupts tut.

    Beim folgenden Durchlauf ist in einer Routine ein Aufruf getCurrent_ThreadContext_Jc() zusätzlich ausgeführt. Dieser ruft intern die Windows-API-Routine GetCurrentThreadId() und sucht danach binär in einem Datenfeld. Das Datenfeld enthält aber im Beispiel nur einen Thread. Damit ist die Rechenzeit von ca. 12 ns fast ausschließlich auf die API-Routine zurückzuführen. Diese ist auch in anderweitigen Beschreibungen als extrem schnell bezeichnet. Es wird lediglich aus dem Systemlevel ein fest gespeicherter Wert geholt. Adäquate API-Routinen werden auch in anderen Multithread-Systemen vergleichbar schnell sein.

    Danach werden insgesamt 31 ThreadContexte mit fiktiven Thread-Id addiert, so dass die Suche nach dem ThreadContext länger dauert. Die Ids liegen so, dass in der binären Suche auch tatsächlich die maximale Anzahl von 6 Suchschritten in einer Schleife notwendig sind. Damit verschlechtert sich die Bilanz von getCurrent_ThreadContext_Jc().Bei 32 Threads sind es nunmehr 22 bis 25 ns anstatt 12 ns. Bei 1000 Threads werden es aber nur ca. 35 ns sein, da aufgrund der Organisation als Binäre Suche die maximale Anzahl der Suchdurchläufe nur logarithmisch steigt, im Beispiel max. 10. Auch 35 ns sind für Anwendungen im ms-Raster eigentlich nichts. Für ein kleines Multithreadsystem auf einen langsamen Prozessor steht die Frage nicht, wenn man den Threadcontext dort direkt organisiert.

    Danach wird eine andere Zwischenroutine gerufen, in zwei Ebenen wird dabei auf den Stacktrace verzichtet. Das spart zusammen 11 bis 13 ns. Demzufolge dauert die Einrichtung eines Stacktraces ohne die Notwendigkeit, den ThreadContext erst zu ermitteln, ca. 6 ns. Diese Zeit ist wesentlich. Auf einem Prozessor mit 20 MHz Takt könnte es ca. 1 Mikrosekunde sein, Es handelt sich um wenige Befehle. Für extrem schnelle Routinen also nicht besonders geeignet. Aber in den langsamen Zeitschalen, in dem auch komplexe Dinge ausgeführt werden, unbedeutend. Dort sollte der Stacktrace grundsätzlich angewendet werden, dort ist er gegebenenfalls nötig.

    Danach wird aufgrund anderer Datenlage ein THROW ausgelöst, in C mit einem longjmp() ausgeführt. Zu bedenken ist, dass der Weg der Bearbeitung insgesamt anders läuft. Das THROW dauert etwas länger als die hier gemessenen 114 ns, weil im Normalfall stattdessen mindestens zwei return-Befehle ausgeführt werden. Doch es sind kurze Zeiten.

    In der letzten Messung wird das THROW mit den zwei eingesparten Stacktraces ausgeführt, es ist damit ca. 12 ns kürzer. Das liegt daran, dass beim THROW der Stacktrace bis zur TRY-Ebene aus dem Stack umkopiert werden muss. Angenommen, es liegen 100 Aufrufebenen zwischen TRY und THROW, dann wären dafür ca. 600 ns notwendig. Andererseits muss es schon ein komplexer Algorithmus sein, der 100 Ebenen aufruft. Bei Rekursionen entsthehen viele Ebenen. Auf einen 20-Mhz-Prozessor bezogen könnten das dann schon 60 bis 200 Mikrosekunden sein. Ungünstig ist ein THROW, wenn in schneller Rechenzeit sehr viele Rekursionen ausgeführt werden, die in einer unteren Ebene zum THROW führen. In allen anderen Fällen sollte hier problemlos stehen.

    Das selbe Programm wurde unter C++ mit Nutzung des echten throw programmiert und zeigte damit folgende Ergebnisse:

    d:vishiaJcTest_Msc6TestCppSimpleRelease>TestCppSimple.exe Test_Exception, a=6, =>IndexOutOfBoundsException_Jc

     Exception: The max.expected index is 5: 6
       at testExceptionThrow (D:\vishia\Jc\Test_Msc6\src\Test_Exception.c:102)
       at testException (D:\vishia\Jc\CRuntimeJavalike\Exception_Jc.c:68)
       at main (D:\vishia\Jc\Test_Msc6\src\Test_main.c:44)
     finally, call everytime in try block.
     Test Exception calculation times in C++
     test normal ... calcTime = 56.300000
     test normal ... calcTime = 46.800000
     test normal ... calcTime = 48.500000
     test additional getCurrent_ThreadContext_Jc() ... calcTime = 59.300000, add = 10.800000
     add some dummy thread Contexts to increase the list of threadContextes
     test normal ... calcTime = 48.500000
     test additional getCurrent_ThreadContext_Jc() ... calcTime = 71.900000, add = 23.400000
     test without 2 Stacktrace levels ... calcTime = 39.000000, add = 9.500000
     test with throw ... calcTime = 3440.000000, add = 3391.500000
     test with throw without 2 Stacktrc levels... calcTime = 3280.000000, add = 160.000000
    
     d:\vishia\Jc\Test_Msc6\TestCppSimple\Release>TestCppSimple.exe
     Test_Exception, a=6, =>IndexOutOfBoundsException_Jc
    
     Exception: The max.expected index is 5: 6
       at testExceptionThrow (D:\vishia\Jc\Test_Msc6\src\Test_Exception.c:102)
       at testException (D:\vishia\Jc\CRuntimeJavalike\Exception_Jc.c:68)
       at main (D:\vishia\Jc\Test_Msc6\src\Test_main.c:44)
     finally, call everytime in try block.
     Test Exception calculation times in C++
     test normal ... calcTime = 54.700000
     test normal ... calcTime = 48.400000
     test normal ... calcTime = 46.900000
     test additional getCurrent_ThreadContext_Jc() ... calcTime = 59.300000, add = 12.399998
     add some dummy thread Contexts to increase the list of threadContextes
     test normal ... calcTime = 48.400000
     test additional getCurrent_ThreadContext_Jc() ... calcTime = 71.900000, add = 23.499998
     test without 2 Stacktrace levels ... calcTime = 37.500000, add = 10.900002
     test with throw ... calcTime = 3440.000000, add = 3391.599998
     test with throw without 2 Stacktrc levels... calcTime = 3440.000000, add = 0.000000
    
     d:\vishia\Jc\Test_Msc6\TestCppSimple\Release>
    

    Das sind ebenfalls wieder zwei Messungen nacheinander. Dazu ist folgendes festzustellen:

    Die Zeiten sind etwas schneller als in C. Das verwundert, vom Quellcode ist C++ keinesfalls optimaler gestaltet. Es muss daran liegen, dass der C++-Compiler des Visual Studio 6 für C nicht ganz so optimal arbeitet. Sehr unterschiedlich sind die Zeiten aber nicht.

    Das THROW dauert aber extrem viel länger: ganze 3 Mikrosekunden gegen reichliche 100 ns des longjmp(). Wie throw vom Compiler umgesetzt wird, mag sehr compilerspezifisch sein. Es könnte günstig sein, dass man die Wahl zwischen throw und longjmp() auch bei C++ hat (Destruktor-Problem beachten).

    Das die Zeitmessung für die ausgelassenen Stacktraceebenen beim throw stark schwankt, ist auf Schwankungen des throw selbst zurückzuführen. Die Messung ist also nicht relevant. Die ausgeführten Befehle sind in C++ etwa die gleichen wie in C.

    Als Gesamtbilanz kann folgende Einschätzung gegeben werden:


    6 Home, Links

    Topic:.finishJc.

    Dieser Artikel wurde geschrieben von Dr. Hartmut Schorrig.


    Topic:.hrefJc.Exception.