while abbrechen in Objekt orientierter Programmierung



  • Hallo.
    Es geht darum:
    Arbeite mit Borland Builder und habe ein C++ Programm auf Basis der OOP Objekt orientierten Programmierung gemacht. Hier gibt es unter anderen auch ein Button-Objekt welches ich mit "START" bezeichne, das wenn es angeklickt wird, eine gekapselte while-Schleife startet. Diese Schleife besteht aus 2 while Schleifen. Die erste liest immer eine Zeile aus einer Datei aus und sendet diese über USB an ein externes Gerät. Dann geht es in die gekapselte zweite Schleife, wo auf das OK,\r,\n vom externen Gerät gewartet wird. Kommt OK, dann geht es zurück in die erste Schleife, eine neue Zeile wird eingelesen und gesendet usw.. Kommt ER als Error, werden beide Schleifen gestoppt. Das alles funktioniert sehr gut.

    Verbesserungsgedanke:
    Wenn von dem externen Gerät aber aus irgendeinen Grund kein OK und auch kein ER kommt, hängt sich das Programm in der Schleife, die auf OK oder ER wartet, auf! Dafür möchte ich ein weiteres Button-Objekt als "STOP" in das Programm einfügen, welches "per-Hand" die while-Schleifen ebenfalls unterbrechen kann.

    Lösungsversuch:
    Ich generiere eine bool-Variable, die durch das Button-Objekt "STOP" auf "false" gesetzt wird. Anhand der verwendeten bool-Variable, habe ich sogar die Möglichkeit zu erkennen, ob der Abbruch der while Schleife durch Empfang von ERROR oder per Hand geschehen ist. Der Gedanke war schon ok, aber...

    Das Problem:
    Sobald ich mit dem Button-Objekt "START" die while-Schleife starte, habe ich keinen Zugang mehr zu all den anderen Objekten, die ich in das Programm eingebaut habe. Auch mein schönes Button-Objekt "STOP" lässt sich nicht mehr anklicken. Ist ja auch irgendwo logisch. Denn solange der Prozessor in der while Schleife verharrt, kann er nicht die anderen Objekte bedienen bzw. verarbeiten.

    Nun die Frage:
    Es ist mir schon klar, das ich innerhalb der while Schleife eine Abfrage dieses Button-Objekts "STOP" machen muss, damit dieses überhaupt funktioniert. Aber wie kann man in der OOP das Button-Objekt "STOP" in die while Schleife so integrieren, das man die Möglichkeit hat, den Button STOP noch anzuklicken?
    Die normalen C-Befehle wie "cin", "getch()" usw. funktionieren in der OOP ja nicht! Sonnst könnte man ja eine Tastenabfrage wie "ESC" anstelle eines Buttons machen und die OnKeyDown, oder OnKlick Ereignisse der Objekte funktionieren ja auch nicht, solange die while Schleife am laufen ist.

    Hat da jemand eine Idee wie man unter OOP eine while Schleife durch ein Button-Objekt o.ä. beenden kann????
    Danke



  • Das hat jetzt weniger bis gar nichts mit OOP zu tun.
    Du musst Code, der länger zum Ausführen braucht, asynchron ausführen und die GUI nicht blockieren. Gibt glaub z.B. irgendwelche Guidelines von Microsoft, dass alles was länger als 50ms dauert, in einem Seitenthread ausgeführt werden sollte.
    Musst dich also etwas mit nebenläufiger Programmierung beschäftigen.

    Alternativ könnte man als Hack die GUI auch veranlassen, die Nachrichtenschleife abzuarbeiten. Ich weiß nicht mehr, wie das in der VCL ging. Hab jetzt schnell über google Application.ProcessMessages gefunden, scheint das richtige zu sein. Kommt mir ehrlich gesagt nicht bekannt vor.
    Wär aber wie gesagt ein Hack, man sollte sich auf solche Lösungen von vornherein nicht einlassen.



  • @Eric Grundsätzlich hat Dir ja Mechanics ja schon geschrieben, wie Du Dein Problem lösen kannst. Von mir gibt's noch ein paar Details:
    Es gibt eine schöne, aber etwas schwierigere Variante, und eine hässliche aber einfache Variante. Letztere hat sogar den Vorteil, daß sie auch unter 16-Bit Windows funktionieren würde. Ich nehme aber mal an, das brauchst Du nicht. Deswegewn gehe ich darauf nicht näher ein.

    Threads sind in der VCL recht einfach zu implementieren:
    Erzeuge eine von TThread abgeleitete Klasse und implementiere dort was auch immer Du brauchst.
    Der OnClick-Handler deines Startbuttons muß überprüfen, ob der Thread schon läuft, wenn nein, kann er ihn starten. Dein Stopbutton setzt irgendeine Statusvariable, auf die Dein Threadobjekt mindestens lesenden Zugriff hat. Deine beiden Schleifen benutzen diesen Status als Abbruchkriterium.
    Wenn Du's ganz perfekt machen willst, benutzt Du für Deine Statusvariable eine Atomic-variante. Die VCL hat aber leider meines Wissens soetwas nicht. Bei einem einfachen Bool genügt es aber auch zur Not, diese volatile zu machen.

    mfg Martin



  • Dafür gibt es die TThread.Terminate()-Funktion und die Eigenschaft Thread.Terminated, die man dann im Thread selbst abfragen kann.



  • @Th69 Das ist natürlich noch besser. Hab ich gestern nicht daran gedacht.



  • Hallo und Danke für eure Hilfe.
    Jetzt weiß ich wenigstens in welche Richtung ich mich orientieren muss. Auf meiner Internet Suche nach einer Lösung für mein Problem, bin auch schon mal auf den Begriff "Thread" gestoßen. Kann aber leider damit noch nichts anfangen. Muss mich hier erst einmal einarbeiten.
    Aber eure Antworten geben zumindest die richtige Richtung vor.
    Danke und Gruß



  • Hallo nochmal...
    Also ich habe mich jetzt mal über Threads grob schlau gemacht. Im Borland Builder gibt es unter "DATEI" > "NEU" eine Auswahl "Thread Objekt". Damit ist es sehr einfach einen Thread zu erzeugen. Dieser funktioniert nun auch schon so, wie es sein sollte. Habe zum Test in den Thread nur eine Endlos-while-Schleife eingeschrieben und es lässt sich nun auch schon mit einem Stop-Button wieder zurück zu den anderen Objekten kehren. Ob die Endlos-while-Schleife im Hintergrund dann noch weiter macht, oder ob diese beendet wird, weiß ich nicht. Ist aber erst einmal egal, da das Programm nur zum Testzweck geschrieben wurde.

    Eine weitere Frage:
    Jetzt möchte ich ja die Schleife mit einer bool Variable ordentlich beenden. Die Schleife in dem Thread kennt aber den Variablennamen nicht, obwohl ich diesen für das gesamte Programm im Programmkopf deklariert habe. Der Thread kennt aber die deklarierten Variablen und auch die eingefügten Objekte nicht!
    Wie kann ich dem Thread die Deklaration übergeben, damit dieser die gleichen Deklarationen nutzen kann, wie im Hauptprogramm?



  • Wie ich schon schrieb, benutze die Terminated-Eigenschaft:

    while (!Terminated)
    {
      // ...
    }
    


  • @Th69 sagte in while abbrechen in Objekt orientierter Programmierung:

    Dafür gibt es die TThread.Terminate()-Funktion und die Eigenschaft Thread.Terminated, die man dann im Thread selbst abfragen kann.

    Cooperative Thread Cancellation über an den Thread gebundene Standardmittel (die z.B. ein Framework bereitstellt) ist meist keine gute Idee. Weil dabei keiner genau weiss wie Funktionen aus diversen Libraries die man verwendet darauf reagieren. Nehmen wir an wir verwenden in dem Thread zwei Funktionen aus Third-Party Libs, Outer() und Inner(). Outer() ruft Inner() auf. Inner fragt das "cancel flag" ab, und denkt sich "ah, ich bin gecancelt, ich hör jetzt auf obwohl ich nocht nicht erledigt haben was ich erledigen hätte sollen" -- erfüllt also ihren Contract nicht. Outer() dagegen weiss davon nix und verlässt sich darauf dass Inner() ihren Contract erfüllt hat.

    Natürlich könnte man das "fixen" indem man alle Contracts auch bezüglich Cancellation penibel genau ausführt. Die Erfahrung zeigt aber dass es kaum jemanden schert genau definierte Contracts zu formulieren -- oder auch nur die Contracts von Third Party Libs die man verwendet genau zu lesen.

    => Besser eigene Cancel-Flags machen.



  • @hustbaer Wenn es genügt, daß TThread::Execute() das Terminate-Flag auswertet, kann man das schon so machen. Thirdparty-Libs werden i.d.R. von TThread eher nichts kennen, außer sie sind speziell für die VCL implementiert.



  • Zumal TThread eine Methode zum Synchronisieren mit dem GUI-Main Thread mitbringt. Damit lässt sich die GUI auf die Schnelle aus dem Thread aktualisieren, ohne dass man was eigenes mit Locks bauen müsste.



  • Hi Eric,

    im Builder gibts die Funktion
    Application->ProcessMessages();
    Die sorgt dafür das Nutzereingaben ausgewertet werden. Also in der onclick-Routine des Buttons eine Schaltervariable setzen und in der Schleife abfragen ob die Schalterwariable schon gesetzt wurde. Dies kann ganz einfach in jeder while-Bedingung... geschehen.
    Nicht vergessen nach dem Abarbeiten die Schaltervariable ggf zuurückzusetzen.

    Gruß Mümmel



  • @mgaeckler sagte in while abbrechen in Objekt orientierter Programmierung:

    @hustbaer Wenn es genügt, daß TThread::Execute() das Terminate-Flag auswertet, kann man das schon so machen.

    Klar kann man. Ich wollte halt anmerken dass Cooperative Thread Cancellation über Boardmittel von Frameworks im Allgemeinen eine schlechte Idee ist. In dem Sinn dass es Probleme gibt sobald es als "das" Mittel der Wahl angesehen und von vielen Komponenten verwendet wird.

    Thirdparty-Libs werden i.d.R. von TThread eher nichts kennen, außer sie sind speziell für die VCL implementiert.

    Soll ja vorkommen dass jemand Libraries/Komponenten entwickelt die auf Frameworks ala VCL aufsetzen. Mir ist beim Suchen von passenden fertigen Libs/Komponenten öfter mal ein Projekt untergekommen das gut gepasst hätte aber dummerweise auf die VCL aufbaut. (Dummerweise für mich da ich die VCL eben nicht verwende.)



  • Hallo zusammen.

    Den Vorschlag von @Th69 die while Schleife mit "Terminated" zu beenden habe ich auch in meinem Testprogramm so gemacht. Das habe ich auch verstanden wie es funktioniert und stellt auch für mich die beste Methode dar, den Thread dann zu stoppen bzw. beenden.
    Aber es gibt da ja auch noch Variablen die im Hauptthread (Hauptprogramm) deklariert wurden und Objekte die im Hauptthread angelegt wurden.
    Da kommt die Antwort von @muemmel mit "Application->ProzessMessages();" schon gut. Aber soweit ich das verstanden habe, werden hier nur die eintretenden Ereignisse aus dem Hauptthread (Hauptprogramm) abgefangen und könnten im Thread verarbeitet werden. Hmmm das ist schon gut zu wissen, wie ich z.B. einen Tastendruck oder Mouseklick erkennen kann. Deshalb Danke für den Hinweis. Aber das löst leider das eigentliche Problem noch nicht.
    Deshalb habe ich mir die Antwort von @DocShoe mit dem "Synchronize();" angesehen. Ich muss gestehen, auf der Seite
    "http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/!!OVERLOADED_Synchronize_Classes_TThread.html"
    Gibt es zwar Info darüber, aber da blicke ich im Moment überhaupt nicht durch. Wann und wo und wie nutzt man das Synchronize? In meinem schlauen Buch "C++ Builder das Kompentium" steht dieses da auch nicht beschrieben. Lediglich, das man Synchronize nicht aus dem Hauptthread (Hauptprogramm) aufrufen soll, da dieses zu Endlosschleifen führen kann. Es wäre schön, wenn mal jemand hierzu ein kleines einfaches Beispiel geben könnte.

    Aber einen Teil meiner zweiten Frage habe ich bereits selber herausfinden können. Wie man die Objekte aus dem Hauptthread in den eigentlichen Thread mit übernehmen kann.
    Es war natürlich mein Fehler... denn ich habe in den Thread einfach das selbe eingegeben wie im Hauptthread auch.

    Label1->Caption = "Thread running";
    

    Richtig muss es aber so gemacht werden...

    Form1->Label1-Caption = "Thread running";
    

    Dann funktionierst auch! Ist mir auch jetzt klar. Ein Thread ist halt in der selben Ebene angesiedelt, wie das Hauptprogramm auch und nicht ein Teil des Hauptprogramms, welches unter "Form1" seinen Dienst tut.

    Aber, da wäre jetzt nur noch eine Sache. Das mit den Variablen. Wenn ich z.B. im Hauptprogramm eine unsigned char- Variable namens "SendByte" erzeugt habe, und diese im Thread z.B. noch verändert werden muss, dann kennt der Thread diese Variable nicht. Ich kann aber auch nicht schreiben...

    Form1->SendByte = SendByte + Bit5; //Bit5 = Wert, um das Bit 5 in SendByte zu setzen!
    

    Jetzt stellt sich die Frage, wie kann ich die Variable "SendByte" für den Thread sichtbar bzw. zugänglich machen?
    Wenn mir jemand sagen könnte wie man das macht, würde ich schon weiter kommen.
    ODER! kommt hierbei das "Synchronize();" zum Einsatz was @DocShoe schon erwähnte?
    Danke allen die sich hier doch sehr hilfreich beteiligen.
    Gruß Eric



  • Application->ProcessMessages() ist ein wahrscheinlicher Grund für plötzlich auftretende Schutzverletzungen und seltsames Verhalten. Solange man nicht genau weiß, wann und warum man das benutzt sollte man es nicht benutzen. Wenn´s irgendwie anders geht: Mach´s anders!



  • Multithreading ist kein einfaches Thema, obwohl´s erst ein Mal einfach aussieht.

    1. du darfst aus einem nebenläufigen Thread keine GUI Elemente aktualisieren. Das kann funktionieren, muss aber nicht.
    2. du musst gemeinsam benutzte Daten die Zugriffe gegeneinander verriegeln (nur wenn min. ein Schreibzugriff dabei ist)

    Zu 1)
    Dazu benutzt du die Synchronize() Methode von TThread.

    Zu 2)
    Der Zugriff auf die Daten muss per CriticalSection oder Mutex geschützt werden. Vor dem Zugriff rufst du Enter auf, wenn du damit fertig bist Leave.

    Das Ganze könnte in etwa so aussehen (C++98, keine Ahnung, ob du einen C++ Builder hast, der C++11 unterstützt). Alles primitiv und ohne RAII, da ist noch Luft nach oben.

    // mydata.h / mydata.cpp

    struct MyData
    {
    ....
    };
    

    MyThread.h

    #include "mydata.h"
    // fehlende Includes einfügen
    
    // Signatur für Callback
    typedef void (__closure* CallbackSig)( MyData& data );
    
    class MyThread : public TThread
    {
       MyData Data_;
       TCriticalSection* CriticalSection_;
    
    public:
       CallbackSig   OnDataChanged = nullptr;
    
    public:
       __fastcall MyThread();
       __fastcall ~MyThread();
    
       MyData& data();
       void lock();
       void release();
    
    private:
       void __fastcall Execute();
       void fire_data_changed() const;
    };
    

    MyThread.cpp

    #include "mythread.h"
    
    __fastcall MyThread::MyThread  :
       TThread( true ), // CreateSuspended
       CriticalSection_( new TCriticalSection() )
    {
       // Thread soll sich selbst löschen, wenn er die Execute-Methode verlässt
       AutoDelete = true;
       ...
    }
       
    __fastcall MyThread::~MyThread  
    {
       delete CriticalSection_;
    }
    
    void MyThread::lock()
    {
       CriticalSection_->Enter();
    }
    
    void MyThread::release()
    {
       CriticalSection_->Leave();
    }
    
    MyData& MyThread::data()
    {
       return Data_;
    }
    
    void __fastcall MyThread::Execute()
    {
        while( !Terminated )
        {
           // magic happens here
    
           // Zugriff auf Daten sichern
           lock();
           update_data();
           release();
    
           // in den Hauptthread der Anwendung wechseln und dann die Methode fire_data_changed aufrufen.
           // der aktuelle Thread wartet so lange, bis der fire_data_changed Aufruf beendet ist.
           Synchronize( fire_data_changed );  
        }
    }
    
    void MyThread::fire_data_changed() const
    {
       // Weil diese Methode im Hauptthread der Anwendung aufgerufen wird und der Thread, der
       // die Daten verändert, gerade (quasi) angehalten ist darf auf die Daten ohne Locking zuge-
       // griffen werden (der schreibende Thread schreibt im Moment garantiert nicht!)
       if( OnDataChanged )
       {
          OnDataChanged( Data_ );
       }
    }
    

    MyForm.h

    #include "mydata.h"
    #include "mythread.h"
    
    class MyForm1 : public TForm
    {
       MyThread* WorkerThread_;
    public:
       __fastcall MyForm1( TComponent* Owner );
    
    private:
       void show_data( const MyData& data );
       void on_data_changed( const MyData& data );
       void __fastcall on_timer( TObject* Sender );
    };
    

    MyForm.cpp

    #include "MyForm.h"
    
    __fastcall MyForm1::MyForm1( TComponent* Owner ) :
       TForm( Owner ),
       WorkerThread_( new MyThread() )
    {
       // Callback registrieren. Wenn der Thread die Daten verändert hat ruft er den Callback auf
       WorkerThread_->OnDataChanged = on_data_changed;
    
       // Thread starten
       WorkerThread_->Resume();
    }
    
    void MyForm1::on_data_changed( const MyData& data )
    {
       // der Aufruf findet im Hauptthread der Anwendung statt, daher dürfen hier GUI Elemente
       // verändert werden.
       show_data( data );
    }
    
    void __fastcall MyForm1::on_timer( TObject* sender  )
    {
       // der Zugriff auf die Daten muss synchronisert werden, da der Thread sie möglicherweise
       // in der Execute-Methode gerade verändert.
       WorkerThread_->lock();
       show_data( WorkerThread_->data() );
       WorkerThread_->release();
    }
    
    void MyForm::show_data( const MyData& data )
    {
       ...
    }
    


  • Hallo @DocShoe
    Woooouuuhh... das ist ganz klasse das du mich mit einem Beispielcode unterstützt. Ich muss mir das Ganze jetzt aber erst einmal genauer ansehen. Denn wie du schon geschrieben hast, sieht es einfach aus, ist es aber nicht.
    Der Ganze Aufwand hier, auch mit eurer Hilfe, betreibe ich ja nur, um einen Button aktiv zu halten, der eine Schleife stoppen soll... Ist schon ganz schön großer Aufwand, was man alles neues erlernen und ausprobieren muss um ein eigentlich kleines Ziel zu erreichen. Aber so ist das halt meistens. Wenn man ein Problem gelöst hat, kommen 2 andere hinzu.... Man(n) will es aber auch immer wissen, wie man so etwas lösen kann!
    Aber was ich zumindest auf den ersten Blick sehen kann ist, das eine Struktur mit "struct" angelegt werden muss, damit durch die include Anweisung der Header-Datei der Inhalt der struct in der "Form" wie auch im "Thread" genutzt werden kann. Das probiere ich gleich mal aus und das andere sehe ich mir dann mal Stück für Stück in Ruhe an.... Habe auch schon etwas neues gefunden, wo ich von der Existenz dieses noch nichts wusste: "AutoDelete"
    Das werde ich auch einsetzen.
    Danke für die wirklich gute Hilfe.
    Gruß Eric.



  • @Eric
    AutoDelete gibt´s nur für TThread. Da sämtliche Objekte, die von TObject abgeleitet sind nur auf dem Heap erzeugt werden können hat man bei Fire&Forget Threads keine Möglichkeit, das Objekt später wieder zu löschen, jedenfalls nicht ohne Aufwand. Bei

    class MyThread : public TThread
    {
       __fastcall MyThread () 
       {
          AutoDelete = true;
       }
    
       void __fastcall Execue()
       {
          ...
       }
    }
    
    void async_work()
    {
       new MyThread();
    }
    

    ensteht kein Speicherleck, weil sich der Thread nach Verlassen der Execute Methode selbst wegräumt.



  • @DocShoe sagte in while abbrechen in Objekt orientierter Programmierung:
    ...

      AutoDelete = true;
    

    Heisst das nicht eher:

    FreeOnTerminate = true;
    


  • @Burkhi
    Ääääähhhh... möglicherweise.
    Das war'n Test für Eric 😉


Log in to reply