Erstellung eines Worker-Threads zur Grafikausgabe



  • @DocShoe
    Das habe ich auch schon bemerkt, dass punkte dabei einfach mehrmals vorkommen und auch gezeichnet werden.
    Und an das, was Du sagst habe ich auch schon gedacht.
    Und das werde ich auch noch nachholen, da Warten auf die Anzeige trotzdem für den Anwender sch... ist.
    Pens werden auch schon vorher erzeugt und dann gewechselt.
    Trotzdem muss ich den "Point vorher berechnen und dann vergleichen, ob er schon da war.
    Also es gibt Sachen, die ich auf jeden Fall nacharbeiten werde.



  • Nichts gegen Optimierungen, aber selbst wenn du wirklich gute Optimierungen findest und den Zeichenvorgang auf 10 Sekunden drücken könntest - das exakt gleiche Problem wäre immer noch da.
    Microsoft hat vermutlich auch einige Empfehlungen, wie lange so ein Zeichenvorgang (Edit: also beim Malen in den DC) maximal dauern darf. Mehr als einige Zehntelsekunden sollten es nicht sein.
    Ob ein separater Thread verwendet wird oder nicht: an double buffering führt meiner Meinung nach kein Weg vorbei.
    Und so kompliziert ist das Ganze wirklich nicht.



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

    @DocShoe
    ...
    Trotzdem muss ich den "Point vorher berechnen und dann vergleichen, ob er schon da war.
    ...

    Ne, musst du nicht. Du überlegst dir, wie viele Datenpunkte pro Kurve sinnvoll sind. Wenn dein Diagramm netto 1500 Pixel breit ist macht es keinen Sinn, mehr als 1500 Stützpunkte einzuzeichnen. Wenn du jetzt (der Einfachheit halber) 15.000 Datenpunkte hast kannst die Datenmenge in 10 Blöcke einteilen und aus jedem Block einen Wert bestimmen, den du einzeichnen möchtest. Da muss weiter nix mehr überprüft werden, du musst dir nur überlegen, welche Daten du haben möchtest. Wahrscheinlich willst du weder Minimum noch Maximum eines Block verlieren, also brauchst du vielleicht zwei Werte je Block. Und dann musst du die Anzahl der Blöcke halbieren, weil jeder Block zwei statt einem Wert liefert.



  • @DocShoe
    Ich muss z.B. Events von der Anlage abprüfen. Wenn das innerhalb eines Blockes ist, den ich dann nur "grob überfliege", hab ich schon verloren. Ich kann keinen Messpunkt ignorieren.



  • Datenhaltung (Logik) und Zeichnen sind aber zwei verschiedene Dinge. Und für das Zeichnen legst du dir halt eine eigene Datenstruktur an (mit deutlich weniger Werten).



  • @elmut19
    Wieso das denn? Du sollst keine Werte wegwerfen, sondern dir überlegen, welche tatsächlich anzeigerelevant sind.
    Nochmal:
    Bei einer Diagrammbreite von 1500 Pixeln macht es keinen Sinn, 150.000 Stützpunkte pro Kurve zu zeichnen. Das wären ja in der Breite pro Pixel 100 Linien, da reicht doch Minimum und Maximum, die restlichen 98 Linien sind nur Ballast.



  • @DocShoe
    Das stimmt. Und die 100 identischen Pixel habe ich auch selbst schon gezählt.
    Und ich werde da auch noch was machen.
    Mir schwirrt eben nur schon seit langem diese Thread-Lösung im Kopf rum,
    und ich wollte das einfach auch ausprobieren.



  • Du kannst auch mal messen, ob Polyline schneller ist als LineTo. Wenn das messbar schneller ist kannst du Linien bis zum Farbwechsel en bloc zeichnen.



  • @DocShoe
    PolyLine werd ich mir auch noch ansehn. Danke.
    Jedenfalls sind einige sinnvolle Tipps bei Euren Beiträgen dabei, um die ich nicht herumkommen werde.
    Dafür jedenfalls schon mal vielen Dank an alle.



  • @DocShoe
    Nun habe ich einige Zeit mit dem Thread rumexperimentiert und habe es nun auch hinbekommen, dass es funktioniert.
    Ich habe den Thread so gebaut, wie auch die anderen Threads in der Anwendung.
    Und zumindest ist mir im Verhalten noch nichts negatives aufgefallen.

    Ich wollte nun dieses Thema noch zu Ende bringen.

    Aber vielleicht fällt jemand ja doch noch etwas ein, was ich vielleicht falsch gemacht oder vergessen habe.

    Ich habe hier Deklaration, Aufruf des Thread und die Definition des Thread zusammengestellt.
    Natürlich auf das Wesentliche beschränkt:

    friend	VOID		GrafikThread(LPVOID lpvParam);	// Deklaration als public-Var der Klasse "CViewGrafik" (".h"-Datei)
    
    void CViewGrafik::DrawGrafik(CDC *pDC,pCBaseAnlage pAnlage)
    {
    	...	// einige Vorberechnungen
    	
            // Aufruf des Thread
    	if (m_hGrafikThread) {
    		CleanUpGrafikThread();
    	}
    	if (!m_hGrafikThread) {
    
    		m_bGrafikEnde = FALSE;
    		if (!(m_hGrafikThread = CreateThread(
    			NULL,
    			0,
    			(LPTHREAD_START_ROUTINE)GrafikThread,
    			(LPVOID)this,
    			CREATE_SUSPENDED,
    			&m_dwGrafikThreadID))) {
    			m_bGrafikEnde = TRUE;
    		}
    		if (!SetThreadPriority(m_hGrafikThread, CMRCAN_THREAD_PRIORITY)) {
    			m_bGrafikEnde = TRUE;
    		}
    		if (ResumeThread(m_hGrafikThread) == 0xFFFFFFFF) {
    			m_bGrafikEnde = TRUE;
    		}
    	}
    
    
    // Der Thread selbst, ohne Angabe einer Klasse
    static VOID GrafikThread(LPVOID lpvParam)
    {
        CViewGrafik* pView = (CViewGrafik*)lpvParam;
        CDC	cDC;
        cDC.Attach(GetDC(pView->m_hWnd));
    
        pView->DrawSkalierung(&cDC);
        pView->DrawKurven(&cDC);
    	...
        ExitThread(0);
    }
    

    Die Reduzierung um doppelte Grafik-Punkte ist dann eine andere Sache.
    Die müssen auch wirklich identisch sein, also auch in der y-Achse

    Vielen Dank jedenfalls für Eure Hilfe



  • 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.


Anmelden zum Antworten