Erstellung eines Worker-Threads zur Grafikausgabe



  • Ob das so gutgeht hängt davon ab was die Funktionen DrawSkalierung und DrawKurven machen. (BTW: Bitte nicht Englisch und Deutsch mischen, das tut weh.)



  • @hustbaer sagte in Erstellung eines Worker-Threads zur Grafikausgabe:

    BTW: Bitte nicht Englisch und Deutsch mischen, das tut weh.

    Sorry. Bin eigentlich auch Deiner Meinung. Aber das hat mein Vorgänger schon so gemacht. Genau diese Fktn stammen von ihm. Ich führe (leider) seinen Stil fort (um keinen Stilbruch zu begehen). Und irgendwann ist es einem egal, weil es immer was wichtigeres gibt.



  • @hustbaer
    Bisher wurde der Device Context als Member der Klasse angegeben.
    So musste ich natürlich für alle Methoden, die irgendwie innerhalb des Threads aufgerufen werden,
    je nachdem von wo diese aufgerufen werden, auf den jeweils gültigen DC umschalten.
    War ne scheiss Arbeit!



  • @elmut19
    Jetzt hast du zwar alles in einen Thread ausgelagert, malst aber immer noch auf nem DC rum, der dem Thread nicht gehört. Wie hustbär schon sagte kann das gut gehen, muss aber nicht.
    Wenn du das sauber machen möchtest erzeugst du dir einen neuen DC, erzeugst eine Bitmap, malst auf der Bitmap rum und kopierst die Bitmap anschließend per BitBlt auf deinen Ziel-DC. Das Kopieren der Bitmap muss natürlich im Hauptthread passieren, du könntest das Bitmap Handle per SendMessage an dein Hauptfenster schicken, das die Bitmap dann zeichnet. Oder kopiert und zwischenspeichert und beim nächsten Repaint einzeichnet. SendMessage hat den Vorteil, dass der Aufruf erst zurückkehrt, wenn die Nachricht vom Empfänger behandelt worden ist, das synchronisiert den Zugriff auf die Bitmap automatisch.



  • @DocShoe Der DC gehört schon dem Thread. Bloss das Fenster nicht. So lange man nur reinmalt ist das normalerweise kein Problem. Das machen z.B. viele Video-Player so, u.a. einer der Standard Renderer von DirectShow.
    Blöd wird's bloss wenn man anfängt am Fenster bzw. Controls rumzuschrauben.

    Und natürlich kann man sich ein Problem einhandeln wenn das Fenster ein WM_PAINT bekommt während der Thread malt. Nicht dass was crasht, aber das WM_PAINT kann dann halt das übermalen was der Thread gemalt hat.

    Eine Memory-Bitmap als Puffer zu verwenden wäre daher vermutlich gar nicht so verkehrt. Ist aber auch nicht ganz trivial umzusetzen.



  • @DocShoe
    Das hat aber gerade den Effekt, den ich nicht will.
    Ich möchte nicht, dass der Bildschirm mehrere Sekunden leer bleibt und man nicht weiss, ob da noch was kommt oder nicht.
    Es ist doch für den Anwender angenehmer, wenn er schon warten muss, dann auch zu sehen, dass was passiert.
    Ich benötige auch noch eine aktive Skala, damit ich mit dieser entweder über ein Fadenkreuz Werte zu einem
    bestimmten Zeitpunkt anzeigen kann oder mit der Maus in die Grafik hineinzoomen kann.

    Aber was kann denn schlimmes passieren?

    Ich kann z.B. den "Exit"-Button bedienen, der mich eine Ebene zurück bringt.
    Aber das fange ich ab, indem ich den Thread kille!

    Auch wenn ich vor dem Ende der Ausgabe das Fadenkreuz setze, passiert nichts. Es zeigt die Werte zum Zeitpunkt an.

    Zoomen kann ich auch. Dann wird der laufende Thread auch gekillt.



  • Das ist aber schon die Frage: wem gehört eigentlich ein DC? Dem System würde ich sagen, zumindest wenn nicht das Classbit CS_OWNDC gesetzt wurde, somit jeweils ein DC für die Fenster dieser Klasse angelegt wurde und GetDC immer den gleichen DC liefert.
    Sonst sind sie eigentlich nur temporär zu gebrauchen, auch wenn die Gesamtanzahl vermutlich größer ist als bei frühen Versionen von Windows (da waren es insgesamt 8 oder so).
    In diesem Fall ist die Frage, was eigentlich passiert, wenn in einen DC gemalt wird, nachdem EndPaint bereits aufgerufen wurde (ich gehe jetzt davon aus, dass in den DC gezeichnet wird, den BeginPaint liefert)?

    Die zweite Frage wäre noch, was mit „Thread killen“ gemeint ist...



  • @elmut19 sagte in Erstellung eines Worker-Threads zur Grafikausgabe:

    @DocShoe
    Ich möchte nicht, dass der Bildschirm mehrere Sekunden leer bleibt und man nicht weiss, ob da noch was kommt oder nicht.

    Das hat aber nichts mit Bitmaps zu tun. Und Bitmaps schließen keine Threads aus, im Gegenteil sogar.
    Ich würde bei Benutzeraktionen wie Zoomen oder Ziehen ohnehin nur zur Orientierung das Koordinatensystem zeichnen, nach Abschluss dieser Aktion kann dann alles gezeichnet werden.
    Und dann könnte bspw. eine Progressbar in der Statusleiste den Fortschritt anzeigen.



  • @yahendrik
    jetzt sind wir bei den Dingen angelangt, wo es ans Eingemachte geht
    und die ich auch noch nicht verstanden habe.
    Aber das ist ja auch das wichtige an de Sache, um eine vernünftige Software hinzubekommen.
    Und vermutlich muss ich das auch so nach und nach dahingehend optimieren.

    Meine "Kill"-Funktion:

    void CViewGrafik::CleanUpGrafikThread()
    {
    	TerminateThread(m_hGrafikThread, -1);
    	CloseHandle(m_hGrafikThread);
    	m_hGrafikThread = 0;
    }
    

    Nun habe ich auch teilweise seltsames Verhalten festgestellt:

    1. hatte ich Abstürze, weil meine "DrawGrafik()" kurz nach dem Aufruf über den Button (und bevor der "GrafikThread()" gestartet war), ein weiteres mal ausgeführt wurde.
      Dadurch wurde "InitPen()" nochmals aufgerufen. Das konnte ich durch ein "Sleep(1000)" am Ende von "DrawGrafk()" seltsamerweise beheben.
    2. Bin ich auch noch am Testen.


  • @elmut19
    TerminateThread birgt so einige Gefahren in sich, das solltest du anders lösen. Du könntest eine Klasse um den Thread bauen, damit du eine Abbruchbedingung implementieren kannst:

    class ScopedLock 
    {
       CRITICAL_SECTION& CriticalSection_ ;
    public:
       ScopedLock( CRITICAL_SECTION cs ) :
          CriticalSection( cs )
       {
          // to do: Fehlerbehandlung
          EnterCriticalSection( &CriticalSection_ );    
       }
    
       ~ScopedLock ()
       {
          // to do: Fehlerbehandlung
          LeaveCriticalSection( &CriticalSection_ );    
       }
       // Objekt darf nicht kopierbar sein
       ScopedLock( ScopedLock const& ) = delete;
       ScopedLock( ScopedLock&& ) = delete;
       ScopedLock& operator=( ScopedLock const& ) = delete;
       ScopedLock& operator=( ScopedLock&& ) = delete;
    };
    
    class Win32Thread
    {
       CRITICAL_SECTION CriticalSection_;
       HANDLE Thread_ = 0;
       bool Terminated_ = false;
    public:
       Win32Thread()
       {
          // to do: Fehlerbehandlung
         CreateCriticalSection( &CriticalSection );
       }
    
       ~WinThread()
       {
          DestroyCriticalSection( &CriticalSection );
       }
    
       void terminate()
       {
          ScopedLock lock( CriticalSection_ );
          Terminated_ true;
       }
    
       bool terminated() const
       {
          ScopedLock lock( CriticalSection_ );
          return Terminated_;
       }
    
       void join()
       {
          // auf Threadende warten
          WaitForSingleObject( Thread_, INFINITE );
       }
    
       void run()
       {
          while( !terminated() )
          {
             if( !terminated() ) draw_step_1();
             if( !terminated() ) draw_step_2();
             if( !terminated() ) draw_step_...();
             if( !terminated() ) draw_step_n();      
          }
       }
    }     
    

    So könnte ein Gerüst für ein Win32 Thread aussehen. Du kannst das auch mit std::thread lösen, nur kenne ich mich mit denen kaum aus. Die Idee ist, dass dein Thread so lange läuft, bis du ihm per terminate() signalisierst, dass er sich zum nächstmöglichen Zeitpunkt beenden soll (=> die run() Methode verlässt). Mit join() wartest du darauf, dass er das tut.
    In der run() Methode solltest du in kürzeren Abständen prüfen, ob der Thread beendet werden muss, damit der Thread nicht zu träge auf die Abbruchaufforderung reagiert.
    Der Übersicht halber ist das Ganze ohne Netz und doppelten Boden.



  • @DocShoe
    Vielen Dank DocShoe.
    Bin nun zurück aus dem Wochenende.
    Nun muss ich mir das Ganze durch den Kopf gehen lassen.

    Also, mein Thread beendet sich selbst ja auch mit "ExitThread(0)", wenn er denn so weit ist.
    Also von dem "TerminateThread()" mal abgesehen.

    Frage zur "CriticalSection":
    Diese sorgt hier wohl nur dafür, dass der Thread nicht ein zweites Mal gestartet werden kann,
    bevor der gerade laufende Thread nicht komplett beendet ist?
    Er schützt sich dann also nur vor sich selbst?
    Das macht sicher Sinn!

    Eine zusätzliche Abfrage in der Ausgabeschleife, ob der Thread von Aussen beendet werden soll, macht sicher auch Sinn,
    da ich ja sonst nicht wissen kann, wann er dann wirklich beendet ist.
    Obwohl ich so vom Verhalten der Software haptisch noch keine Probleme bemerkt habe.
    Aber ausschliessen kann ich es mit meiner Variante wohl nicht.

    Also vielen Dank noch dafür.
    Ich werde mich rantasten.



  • @elmut19
    Nein, eine CriticalSection verriegelt die Ausführung von bestimmten Codeabschnitten für mehrere Threads. Eine CriticalSection kann nur von einem Thread gleichzeitig betreten werden, und solange ein Thread eine CriticalSection betreten hat kann kein anderer Thread diesen Codeabschnitt betreten. Er wartet dann so lange, bis die CriticalSection wieder verlassen wird. Damit wird verhindert, dass ein Thread einen Wert liest, während ein anderer ihn gerade schreibt.
    C++ unterstützt sowas auch mit std::atomic ab C++11, ich weiß aber nicht, wie das intern synchronisiert wird. CriticalSections sind unter Windows wohl das Schnellste.

    Was die Haptik deiner Software angeht:
    Starte doch mal 10.000 oder 100.000 deiner Threads und beende sie mit TerminateThread. Mach dir zu Beginn des Tests einen Snapshot über die Statistik deiner Anwendung (benötigter Speicher, Handles) und vergleich´ das nach Testende. Wenn dir da iwas wegläuft hast du ein Problem, das früher oder später garantiert beim Kunden auftritt 😉



  • Nein, eine CriticalSection verriegelt die Ausführung von bestimmten Codeabschnitten für mehrere Threads.

    Ich benutze schon einige CriticalSections in der Software.
    Momentan fällt mir aber kein anderer Thread ein, wo ich an dieser Stelle noch einen zusätzlichen Schutz einbauen müsste.



  • @elmut19

    Ähm... du hast einen Thread, der die Grafikausgabe macht und den Hauptthread deiner Anwendung. Beide greifen auf gemeinsame Daten zu (im Realfall wohl die anzuzeigenden Daten, in meinem Beispiel nur das Terminate-Flag). Ein Thread schreibt, ein anderer liest. Ohne Synchronisierung führt das iwann zu lustigen Ergebnissen. Das Debuggen ist dann meist weniger lustig. Da brauchste bei Race Conditions einiges an Glück, um den Fall zu finden, wo´s in die Hose geht.



  • @DocShoe
    Ja. Das hoffe ich durch die bestehenden Kriterien schon ausgeschlossen zu haben.
    Aber im Einzelfall muss ich das wohl, wegen geänderter Bedingungen nochmals prüfen.
    Nicht dass ich durch einen Haufen CriticalSections noch einen DeadLock produziere.



  • @DocShoe
    Nun hoffe ich, nach langem Suchen, dass ich eine für dieses Problem gangbare Variante gefunden habe.
    Den Thread starte ich nun über "_beginthreadex()" und "_endthreadex().
    Zum externen beenden verwende ich noch "InterlockedCompareExchange()" und "InterlockedExchange()".
    Diese beiden brechen den Thread jeweils an geeigneten Stellen ab.
    Auch habe ich überprüft, ob durch ein "TerminateThread()" etwas zurückbleiben kann, da ich nicht
    sehr lange auf das "normale" Ende des Thread warten kann.
    Eine kurze Zeitspanne kann ich über setzen eines Event am Ende des Threads warten.
    Da ich ausschliesslich mit Membern meiner "CViewGrafik" arbeite, die anschliessend eh behandelt werden,
    kann hier nichts passieren.
    Auch kann in dieser Programmebene kein anderer Thread auf interne Daten zugreifen.
    So war es vorher auch schon (Zustands-Flags und Crit.Sect.).

    Auf jeden Fall danke noch für die Unterstützung.



  • Dann lies dir mal den Remarks Abschnitt durch.



  • @elmut19 sagte in Erstellung eines Worker-Threads zur Grafikausgabe:

    Da ich ausschliesslich mit Membern meiner "CViewGrafik" arbeite, die anschliessend eh behandelt werden,
    kann hier nichts passieren.

    Falsch.



  • @DocShoe sagte in Erstellung eines Worker-Threads zur Grafikausgabe:

    Dann lies dir mal den Remarks Abschnitt durch

    Die Remarks habe ich gelesen, auch vorher schon.
    Ich sehe ein, dass es eine heikle Sache ist.
    Die 4 Punkte konnte ich allerdings positiv beantworten.

    Jedenfalls muss ich sicherstellen, dass ich den Thread loswerde!
    Und das muss auch ohne Beeinträchtigung des Anwenders erfolgen.

    @hustsbear
    Aber was ist noch falsch?
    Es ist auch sichergestellt, dass das Thread-Handle geschlossen wird.
    Ich brauche eine Möglichkeit, den Thread nach einer kurzen Zeit zu 100% loszuwerden.
    Ein "WaitFor..(... INFINITE)" ist keine Lösung!
    Ein WaitFor..(... ZWEI_SEKUNDEN) wäre angemessen.
    Das kann ich über den Event am Thread-Ende gewährleisten.
    Und das ist auch drin.
    Und über das Flag, das ich an den Thread sende, wird dieser auch beschleunigt abgebrochen.

    Aber irgendeine Sicherheit muss ich noch behalten.



  • @elmut19 sagte in Erstellung eines Worker-Threads zur Grafikausgabe:

    @hustsbear
    Aber was ist noch falsch?

    Na der Satz den ich zitiert habe:

    Da ich ausschliesslich mit Membern meiner "CViewGrafik" arbeite, die anschliessend eh behandelt werden,
    kann hier nichts passieren.

    Du rufst da drin ja Win32 und MFC Funktionen auf. Diese Funktionen ... machen Dinge. Und wenn sie z.B....

    • diese Dinge zumindest teilweise im Usermode machen und
    • dazu Locks (Critical Sections, SRWLocks, ...) verwenden

    dann kann dir das übel um die Ohren fliegen. (Nur damit das klar ist: Das ist nicht die einzige Möglichkeit wie es zu einem Problem kommen kann, es ist nur ein Beispiel.)

    Wenn du einen Thread mit TerminateThread abschiesst, dann kannst du nicht kontrollieren wo er abgebrochen wird. D.h. er könnte z.B. eine Critical Section gelockt haben. Die bleibt dann gelockt und dein Programm wird halt einfach hängen bleiben sobald die nächste Funktion aufgerufen wird die die selbe Critical Section locken muss.

    Also ist die Schlussfolgerund es "kann hier nichts passieren" falsch. Es kann sehr wohl was passieren.

    Ich brauche eine Möglichkeit, den Thread nach einer kurzen Zeit zu 100% loszuwerden.
    Ein "WaitFor..(... INFINITE)" ist keine Lösung!

    Und genau das ist auch wieder falsch. Ein WaitFor..(... INFINITE) ist genau die Lösung. Du musst halt sicherstellen dass der Mechanismus den du verwendest um den Thread dazu zu bringen sich selbst zu beenden zuverlässig ist.

    Ein WaitFor..(... ZWEI_SEKUNDEN) wäre angemessen.

    Nein! Ich verstehe ja deine Sorge, vor vielen Jahren als ich selbst das erste mal mit diesem Problem konfrontiert war hab ich mir das selbe gedacht. Und die Versuchung ist gross hier ein "Not-Aus" einbauen zu wollen. Nur glaub mir bitte: es ist besser es nicht zu tun. Wenn sich der Thread nicht von selbst beendet, dann hat dein Programm einen Fehler. Den Thread nach ein paar Sekunden abzubrechen macht aber alles nur noch schlimmer. Es maskiert den Fehler, mit einer "Lösung" die die unangenehme Eigenschaft hat selbst wieder Probleme erzeugen zu können. Und noch dazu Probleme die man dann kaum festnageln kann.

    Was du machen kannst wenn du wirklich meinst es sei notwendig: Wenn der Thread sich nach ein paar Sekunden immer noch nicht selbst beendet hat, dann beende einfach den ganzen Prozess. Idealerweise schreibst du vorher noch einen Crash-Dump damit du nachher was hast womit du schauen kannst warum der Thread sich nicht beendet hat: https://docs.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump


Anmelden zum Antworten