Thread für DB Zugriff



  • @junix

    Ich schreibe an ein Diagramkompo mit einer Zeitachse die nach rechts und links bewegt werden kann. (Das User Event)
    Hat der User das event ausgelöst werden die Signale die in dem Zeitbereich dargestellt werden aus der DB geholt und angezeigt.
    Das Dauert halt so seine Zeit 4 * 0.17s pro Signal. Bei einem Signal geht das aber bei bis zu 50 Signalen wird das schon langsamer.
    Das heißt der User würde die Zeitachse scrollen und will immer weiter bis in den Zeitbereich seine Interesses scrollen.
    Dann wäre es ungünstig wenn er bei jedem Scrollen 3s warten müsste.
    Darum Abrechen des Zeichnens, sondern Thread zu
    und mit neuem Zeitbereich neuen Thread auf.

    Oder gibt es eine Möglichkeit einen Thread einzufrieren und ihn mit neuen Werten zu füllen und dann wieder am Anfang aufzutauen?
    Soweit ich weiß geht das leider nicht.

    möre



  • Hallo Möre,

    also fangen wir am Besten noch einmal von vorne an.
    1.)
    Wenn Du den Thread öfter brauchst macht es keine Sinn diesen zu terminieren. Ressourcen sparst Du damit nicht, weil die Byte'chens für Deinen Thread eh im Memory rumliegen.
    2.)
    Einen Thread immer neu zu erzeugen, kostet nicht unerhebliche Systemzeit schon ganz und gar, wenn innerhalb eines Thread's eine DB-Verbindung aufgebaut werden muss (dazu später mehr)
    3.)Spielt hier nicht unbedingt eine große Rolle, aber nur als Tip. Unter Berücksichtigung der Tatsache, das die VCL in Pascal geschrieben ist, sollte man im CBUILDER Objekte im Konstruktor erzeugen und falls man keine auto_ptr oder shared_ptr verwendet diese im Destrukor wieder freigeben.

    Deshalb würde ich Deinen Code wie folgt ändern:

    void __fastcall TForm1::TForm1(TObject *Sender) 
    { // Konstruktor
    
       Thread = new TDBThread(true); //Thread wird suspended erzeugt. D.h., er wird nach dem erzeugen nicht!
          			    	   // ausgeführt 
    
    /* Dieses wird am Anfang der Execute Methode des Threads erledigt (Denke an mein Hinweis ob die
       DB Verbindung thread saved    ist
    -------------------------------------------------------
       DB = new TmySQLDatabase(NULL); 
        DB->DatabaseName = "testdatenbank"; 
        DB->Host = "192.168.2.103"; 
        DB->UserName = ""; 
        DB->UserPassword = ""; 
        DB->KeepConnection = true; 
        DB->HandleShared = true; 
    ------------------------------------------------------
    */
    
    /* Dieses wird nicht benötigt 
    ---------------------------------------
        // Mein Flag was so etwas wie ein Mutex / Critcal Section darstellt 
        // und da VCL-Kompos synchronisiert werden bietet es sich einfach an 
        Memo1->Tag = 0; 
    
        threadRun = false; 
    ---------------------------------------
    */
    } 
    // Destruktor
    void __fastcall TForm1::~TForm1() 
    {
       // Hier den Thread beenden und löschen
       if (Thread ) 
        {
           Thread ->Terminate();  // Thread soll beendet werden
           if (Thread ->Suspended) // wenn der Thread gerade schläft, wird er auch nicht terminieren
               Thread ->Resume();   // deshalb wecke ihn auf
    
           Thread ->WaitFor(); // warte bis thread beendet
           delete Thread 
           Thread = NULL;
        }
    
    }
    // end of methode ---------------------------------------------------
    
    void __fastcall TForm1::Button1Click(TObject *Sender) 
    { 
           if (!Thread->Suspended) 
           { // Thread ist noch in Arbeit. Du kannst jetzt 2 Dinge tun:
     /*
              entweder mit ShowMessage("blah blah") den Anwender darauf aufmerksam machen, das er noch
              einen Moment warten soll oder Du wartest, bis der Thread Suspended als abgearbeitet ist
              Dieses kannst Du durch eine einfache while Schleife erledigen. Die aufwendigere und sichere
              Methode  ist durch ein Event (ist keine Windows Event!) in Verbindung mit einem  
             WaitForSingleObject ( stop_event , u ) zu erreichen. Ich bin mir aber sicher, dass die etwas  einfachere
              Methode für Deinen Anwendungsfall absolut ausreichend ist. Wenn Dich das 
             WaitForSingleObject interessiert, kann ich Dir später dazu  mal was schicken. O.K.?
             Also hier die einfache Methode:
    */
               while (!thread->Suspended)
    	Application->ProcessMessages();
              // hier könntest Du den Thread mit
                 Thread->Resume();  // wieder starten
              // Noch eine Bemerkung zu ProcessMessages(). Dieses brauchst Du normalerweise nicht ! ausführen.
              // Nur in Verbindung mit der Methode Synchronize ist dieses erforderlich. Weil Du, wenn der Thread
              // diese Methode aufruft mit while (!thread->Suspended) in einen deadlock läufst.
           }
           else
              Thread->Resume();
    
    } 
    //---------------------------------------------------------------------------
    
    /* Dazu muss ich sagen, dass ich Funktion von iCounter nicht verstehe, weil der ja immer innerhalb 
    der Query NULL ist. Auch wo Du das Ergebnis Deiner Query auswertest oder darstellst, habe ich nicht gefunden */
    
    void __fastcall TDBThread::Execute() 
    { 
        iCounter = 0; 
    // Hier das Datenbank Objekt erzeugen
    // alle anderen erzeugten Objekte z.B. im Konstruktor des Threads werden im Kontext des Mainthreads
    // erzeugt !!
        DB = new TmySQLDatabase(NULL); 
        DB->DatabaseName = "testdatenbank"; 
        DB->Host = "192.168.2.103"; 
        DB->UserName = ""; 
        DB->UserPassword = ""; 
        DB->KeepConnection = true; 
        DB->HandleShared = true; 
    
        Query = new TmySQLQuery(NULL); 
        Query->Database = DB; 
    
        while(!Terminated) 
        { 
           // Durch eine Parameterübergabe von iCounter  an das SQL-Statement kann man sich das
          // ständige Clear und ein Prepare in der DB ersparen (Zeitfaktor!) Aber wie gesagt den Sinn von
          // iCounter habe ich nicht ganz verstanden
           Query->SQL->Clear(); 
           AnsiString sSQL = "SELECT * FROM history "; 
           sSQL = sSQL + " WHERE item_id < " + iCounter; 
            Query->SQL->Add(sSQL); 
            Query->Open();  // in diesem Moment wo die Query gestartet wird, kannst Du den Thread nicht vorzeititg
                            // beenden. Du musst also solange warten, bis die Query abgearbeitet ist. Aber wie
                            // gesagt ich habe keine Erfahrung mit MySQL und vielleicht gibt es dort eine solche
                            // Möglichkeit
            Query->Close();
            Synchronize(SyncExecute); // hier wirklich nur das Notwendigste zur Darstellung reinpacken !
    
            Suspend(); // Thread wird schlafen gelegt und durch Resume ggf. wieder zum leben erweckt.
                             // Dabei prüft er zuerst (while Terminated) ob er sich beenden soll
        }
    } 
    //---------------------------------------------------------------------------
    

    Gruß

    Gerhard

    Edit:
    Bitte die Code-Tags und sinnvolle Zeilenumbrüche benutzen. Danke!
    -Jansen



  • @Gerhard

    zu 1. (macht keinen Sinn Thread zu terminieren)

    Ok mit Suspend(Pause) hört der Thread irgendwo im Ablauf an. Mit Resume(Play) beginnt er an der Stelle weiter zu laufen.
    Wie kann ich bei einem Thread aber (Stop) Aufrufen und dann von Anfang an neu beginnen ? Und wie kann ich wenn sich die Werte des DB-Zugriffes (Select *from history where Timestamp *Im Interval min bis max ist*) ändern, dies dem Thread mitteilen ?

    zu 2. (Thread new kostet Rechenzeit und DB-Connection im Thread auch)

    In meinem Quellcode habe ich die Datenbank dem Thread im Konstruktor übregeben also auch die Connection und habe im Thread nur eine Query erzeugt und ausgeführt. Das mit der Threaderzeugung leuchtet mir wiederum ein, führt aber zu dem Stop-Problem, da die Parameter ja im Konstruktor mit übergeben werden.

    zum Code (Form Destruktor)

    Also kann ein Thread erst gelöscht werden wenn er terminiert ist ?
    und kann ein Thread erst terminiert werden wenn er läuft und nicht schläft ?
    oder??

    zum Code (Button Click)

    Mein Problem ist aber gerade das der Nutzer eigentlich nicht mitbekommen soll
    das ein Datenbankzugriff stattfindet. Also der Nutzer drückt den Button und startet damit die DB-Anfrage. Drückt er wieder den Button wird der Thread auf "Stop" gesetzt (bekommt neu Werte übergeben) und startet erneut.
    usw..
    bis der Nutzer nun ein Stelle findet die ihn interessiert oder eben Zeit findet auf die Datenbank zu warten und sich die Werte ansehen will.

    zum Code (Thread - Execute)

    Wie gesagt DB conection wird schon beim Konstruktor mit übergeben.
    iCounter dient nur dazu das ich den SQL-String anpasse um unterschiedliche Werte aus der DB zu bekommen (ist noch Testphase).
    Im Normalfall bekommt der Thread eine Min und Max Zeit und eine Datenbank und holt die Werte in dem ZeitIntervall aus der DB. Wird der Button gedrückt ändert sich das ZeitIntervall und das Spiel sollt von neuem beginnen.

    Danke

    für die neuen Denkansätze in deinem Code. Werd gleich mal Ausprobiern ob die DB synchron laufen muss oder nicht.
    Ich hoffe jemand hat noch eine Idee zu dem Stop-Problem und wie ich die Parameter dem Thread übergebe. (Also nicht im Konstruktor).

    Vielen Dank
    Möre

    CodeTags = ALT + C



  • Hier meine Kommentare (mit -->) und Tips

    zu 1. (macht keinen Sinn Thread zu terminieren)

    Ok mit Suspend(Pause) hört der Thread irgendwo im Ablauf an. Mit Resume(Play) beginnt er an der Stelle weiter zu laufen.
    Wie kann ich bei einem Thread aber (Stop) Aufrufen und dann von Anfang an neu beginnen ? Und wie kann ich wenn sich die Werte des DB-Zugriffes (Select *from history where Timestamp *Im Interval min bis max ist*) ändern, dies dem Thread mitteilen ?
    --->
    Dein Thread läuft innerhalb der while Schleife gnadenlos bis zum Suspend. Sollte das Property oder Flag Terminated auf true gesetzt sein, wird der Thread beended. Aber eben nicht, wie Du scheinbar glaubst mitten in der Programmausführung, sondern erst wenn z.B. die Methode Work() abgearbeitet ist.
    Es steht Dir frei, innerhalb der Methode Work jederzeit, das Flag Terminated abzufragen um die Methode vorzeitig zu verlassen.
    Aber, wie ich Dir schon mitgeteilt habe wenn Du die Query startest

    Query->Open();
    

    und diese Query dauert z.B. 10 Minuten (ist nicht an den Haaren herbeigezogen) hast Du keine Chance deinen Thread vorzeitig zu beenden. Ausnahme sind einige wenige Datenbanken (Oracle IB7), die es erlauben eine solche Query vorzeitig abzubrechen.

    while (!Terminated)
              {
                Work();
                Suspend();
              }
    

    Also einfach stoppen ist nicht ! Du kannst natürlich neben dem vorhandenen Flag Terminated ein eigenes Flag benutzen, um dem Thread mitzuteilen, dass er z.B. die Methode Work() vorzeitig verlassen soll. Aber nicht vergessen, das geht nicht automatisch. Du musst innerhalb der Methode Work() dieses Flag bei jeder Gelegenheit abfragen um im Fall von true z.B. ein return auszuführen. Dann würde die Methode Work() beendet, der Thread geht schlafen (Suspended) was Du jederzeit abfagen kannst und dann neue Parameter übergeben. Aber wie gesagt, wenn die Query läuft und 10 Minuten dauert, no way of return 😡 !
    Ansonsten unten ein Beispiel wie Du andere Parameter in Form eines neuen SQL-Strings übergeben könntest.

    zu 2. (Thread new kostet Rechenzeit und DB-Connection im Thread auch)

    In meinem Quellcode habe ich die Datenbank dem Thread im Konstruktor übregeben also auch die Connection und habe im Thread nur eine Query erzeugt und ausgeführt. Das mit der Threaderzeugung leuchtet mir wiederum ein, führt aber zu dem Stop-Problem, da die Parameter ja im Konstruktor mit übergeben werden.
    ---->
    Ich habe versucht Dir zu erklären, warum Du dieses bei Datenbankverbindungen die nicht thread saved sind genau nicht machen sollst. Sondern die DB-Verbindung im Kontext deines Threads erzeugen solltest.

    zum Code (Form Destruktor)

    Also kann ein Thread erst gelöscht werden wenn er terminiert ist ?
    --> Das ist absolut Korrekt 🙂

    und kann ein Thread erst terminiert werden wenn er läuft und nicht schläft ?
    oder??
    --> Da Terimated lediglich ein Flag ist (siehe while schleife) ist auch das Korrekt. 🙂
    Denn dieses Flag kann natürlich nicht abgefragt werden, wenn er schläft.

    So und nun zu Deiner Parameterübergabe:
    Im Header File Deines Threads legst Du folgende Methoden an

    void Lock(){EnterCriticalSection(&CS);}
        void UnLock(){LeaveCriticalSection(&CS);}
    

    Im Privatbereich der Thread-Klasse eine critical section

    CRITICAL_SECTION CS;
    

    Damit der Beitrag nicht zu lange wird, diesmal keine Erklärung (nur auf ausdrücklichen Wunsch)

    und Deine Variable, die das SQL-Statement aufnehmen soll

    AnsiString ansistring_SQL_Kommando;
    

    Desweiteren brauchst Du natürlich noch eine öffentliche Methode (public) die das SQL Kommando an den Thread übergibt.

    void SetSQL(const AnsiString _SQL);
    

    Im Programmteil:

    1.) Im Konstruktor Deines Thread muss die critical section initialisert werden:

    InitializeCriticalSection(&CS);
    

    2.)
    Im Destruktor wieder entfernt werden

    DeleteCriticalSection ( &CS );
    

    3.)
    Die Methode SetSQL:

    void TDBThread::SetSQL(const AnsiString _SQL)
    {
    
         Lock();
         ansistring_SQL_Kommando = _SQL;
         UnLock();
    }
    

    Durch das Lock wird sichergestell, dass der Thread nicht in dem Augenblick wo er selber diesen Wert lesen will, darauf zugreifen kann. Unlock erklärt sich ja wohl von selbst

    Nun kannst Du Deiner Query ("Query->SQL->Add(ansistring_SQL_Kommando ); ") jederzeit eine neue Query durch Aufrufen der Methode SetSQL des Threads beim drücken auf den Button übergeben.
    D.h., Du kannst das Suspend nach
    Synchronize(SyncExecute);
    herausnehmen. Durch den Aufruf der Methode SetSQL kannst Du jederzeit ohne Probleme ein neues SQL-Statement übergeben. Aber bedenke, dass der Thread ständig läuft was an sich kein Problem ist aber bedacht werden sollte. Um hier unnötige DB-Zugriffe zu vermeiden kannst Du dieses SQL-Statement zusätzlich abspeichern und bevor Du Query->SQL->Add(ansistring_SQL_Kommando) ausführt durch einen einfachen Vergleich festellen, ob ein neues SQL-Statement vorliegt oder nicht. Wenn nein,
    wird eben Query->Open() nicht ausgeführt.
    Oder aber (meine Empfehlung) Du übergibst Deine neue Query, wartest bis der Thread suspended ist (muss der Benutzer doch nicht merken) und machst ein Resume.
    Für alle denen das zu kompliziert ist noch folgender Tip:
    Unter:
    http://www.mitov.com/
    gibt es eine gute Komponente (TBMThread) von Boian Mitov, die einen viel Arbeit abnimmt. Aber keinesfalls die Mühe sich hinter die Geheimnisse und Fallstricke von Threads einzuarbeiten

    Gruß
    Gerhard



  • @Gerhard

    Vielen Dank für deine Unterstützung und jetzt seh ich Threads wieder etwas klarer. Du hast auch vollkommen recht das jeglicher DB-Zugriff egal ob über Threads oder Sequentiell einfach seine Zeit braucht oder einen riesigen Buffer.

    Ich werd jetzt meine Kompo mit einem PleaseWait-Dialog versehen und dann in einem Thread die DB-Zugriffe machen, so das der User mit dem Dialog rumspielen kann.

    Ich danke dir für deine Beiträge und Sorry das ich nicht immer gleich alles verstanden habe.

    Tschau Möre



  • Hallo Möre,

    Du must Dich nicht entschuldigen, wenn Du nicht gleich alles verstanden hast. Ich habe in meinem bisherigen Berufsleben aich nicht immer alles gleich verstanden. Und dummerweise geht es mir heute auch noch so. Also, kein Problem. Gut finde ich, dass Du Dich mit solchen Problemen auseinandersetz !
    Ansonsten noch folgender Tip: Ein großer Buffer lößt auch nicht alle Probleme, (wird gerne bei Access gemacht)da ja erst einmal alles über Netz gezogen werden muss. Nun stell Dir mal vor, Du bist über ISDN mit Deinem Netzwerk verbunden und musst etliche MByte ziehen ?
    Also, keine Scheu vor weiteren Fragen und viel Erfolg

    Gruß

    Gerhard



  • @Gerhard

    Dank dir hab ich jetzt eine Lösung für das Problem gefunden die aktzeptabel ist. Wie schon oben beschrieben der Nutzer ist einfach zu ungeduldig um auf die Datenbank-Abfrage zu warten und scrollt in eine anderen Zeitbereich.

    Die Lösung ist es, dem Anwender einfach ein bisschen Raum für seine Entscheidung zu geben.

    // von Main.cpp im ButtonClick Event

    if (Thread) Thread->Terminate();
    Thread = new TSignalThread(DataBase,DrawingArea);
    Application->ProcessMessages();
    Thread->Priority = tpIdle;
    

    Der Thread ist als Gloabler Zeiger deklariert und wird bei jedem Mausklick
    neu überschrieben. Also immer der Letzte Thread wird terminiert und eine neue Instanze angelegt.

    // von Thread.cpp in der Execute Fkt

    Sleep(750);
    if (!Terminated)
    Synchronize(PaintSignal);
    Terminate();
    

    Wird die Funktion Aufgerufen legt sich der Thread erstmal schlaffen. Nun kann der Nutzer fröhlich weiter scrollen und somit andere Threads erzeugen.
    Wacht der Thread jetzt auf stellt er durch Terminate==true fest, ich werd nicht mehr gebraucht und beendet sich.

    Hält der Anwender jetzt mit seiner Scrollwut inne (oder ist an dem Zeitbereich angekommen der ihn eigentlich interessiert ) wacht der letzte Thread auf, führt die Datenbankabfrage durch und zeigt die Daten an.

    Nun jetzt könnte jemand sagen. Na da halte ich den User ja unötig auf !
    Das ist schon richtig, aber mit einem ständigen Datenbankzugriff nach seinem Scroll würde ich ihn garantiert aufhalten. siehe Gerhard -> bis 10 min.

    Der Datenbankzugriff ist mit dieser Methode immer nur für eine Thread möglich und somit brauchte ich nicht CriticalSection's verwenden.

    Vielen Dank nochmal
    und jetzt bin ich happy 🙂

    Tschau Möre


Anmelden zum Antworten