GUI & ein(!)Thread

Anfängerfragen zum Programmieren mit PureBasic.
Benutzeravatar
Muttonhead
Beiträge: 20
Registriert: 25.06.2017 14:06
Computerausstattung: I7

GUI & ein(!)Thread

Beitrag von Muttonhead »

Hallo @all
Ich versuche im Moment in meinem Spiel das "Hauptprogramm" von der GUI zu lösen und meine Erfahrungen mit Threads generell waren bisher eher als bescheiden zu betrachten.
Hier mal mein Beispielcode:

Code: Alles auswählen

EnableExplicit

Declare MyThread (*junk) 

Structure THREADVars
  tmutex.i
  IsRunning.i;Statusvariable ob Thread noch läuft, soll von GUI überprüft werden.
  
  ;interne Variablen des Threads, nicht unbedingt mutexgeschützt
  ExitThread.i;threadinternes Abbruchsignal
EndStructure


Structure GUIVars
  gmutex.i
  ThreadBreakSignal.i;"GUIWunsch" für Beenden des Threads, soll vom Thead überprüft werden
  
  ;interne Variablen der GUI, nicht unbedingt mutexgeschützt
  ;a.i
EndStructure

;Mutex
Define.i tmutex,gmutex

Define gvars.GUIVars, tvars.THREADVars
;Mutex erzeugen
tvars\tmutex= CreateMutex()
gvars\gmutex= CreateMutex()

Define Event.i


If OpenWindow(0, 0, 0, 300, 100, "GUI vs. Thread", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  TextGadget(1, 10, 10, 250, 20, "Thread läuft nicht")
  ButtonGadget(2, 10, 40, 120, 20, "Starte Thread")  

  Repeat
    Event = WaitWindowEvent()
    
    Select Event
   
      Case #PB_Event_Gadget
        Select EventGadget()          
          Case 2
            LockMutex(tvars\tmutex)
            LockMutex(gvars\gmutex)   
              If tvars\IsRunning=0 ;wenn KEIN Thread läuft  
                gvars\ThreadBreakSignal=0
                CreateThread(@MyThread(), 0)
                SetGadgetText(2, "Beende Thread")
              
              Else                ;wenn Thread läuft 
                gvars\ThreadBreakSignal=1
             
              EndIf
            
            UnlockMutex(gvars\gmutex)
            UnlockMutex(tvars\tmutex)             
          
          
      EndSelect
    EndSelect
  Until Event = #PB_Event_CloseWindow   
EndIf


Procedure MyThread (*junk)
  Shared gvars,tvars
  
  Define.l start
  Define.i ExitThread=0   
  
  LockMutex(tvars\tmutex)   
    tvars\IsRunning=1 ;wenn Thread läuft, dies nach aussen anzeigen  
  UnlockMutex(tvars\tmutex) 
  
  tvars\ExitThread=0
  
  Repeat

    ;die "Alle 5 Sekunden Lang Schwer Beschäftigt ohne Abbruchmöglichkeit Simulation"  
    ;trotz GUIwunsch wird Thread weiterlaufen
    SetGadgetText(1, "Thread heavy")
    start=ElapsedMilliseconds()
    Repeat
    Until ElapsedMilliseconds()>start+5000
    
    ;die "Alle 5 Sekunden Lang Leicht Beschäftigt Mit Abbruchmöglichkeit Simulation"
    SetGadgetText(1, "Thread easy")
    start=ElapsedMilliseconds()
    Repeat   
      LockMutex(gvars\gmutex)    
        If gvars\ThreadBreakSignal=1  ;wenn GUI den Thread beenden will
          tvars\ExitThread=1          ;setze threadinternes Abbruchsignal
        EndIf
      UnlockMutex(gvars\gmutex)         
    Until (ElapsedMilliseconds()>start+5000) Or (tvars\ExitThread=1)
        
  Until tvars\ExitThread=1
  
  ;Aufräumen
  LockMutex(tvars\tmutex)   
    tvars\IsRunning=0 ;wenn Thread beendet, dies nach aussen anzeigen  
  UnlockMutex(tvars\tmutex)
  
  SetGadgetText(1, "Thread läuft nicht")
  SetGadgetText(2, "Starte Thread")
  
EndProcedure
meine Fragen hierzu:
Ist dieses Konstrukt einigermaßen threadsave?
Und wie sieht es mit dem Zugriff auf die GUI aus dem Thread heraus aus? Ich beziehe mich mal auf jene Befehle die das Aussehen oder den Status der Controls verändern.
Ausserdem habe ich das Gefühl,dass man schnell mal gewillt ist im Thread eine Art Arbeitskopie von Variablen zu erzeugen, um dem mutexen etwas aus dem weg zu gehen :)
Mutton
PureBasic 5.62 (Windows 10 Home - x64) | i7 7700HQ | 32GB | HD Graphics 630 / GeForce GTX 1060 Max-Q
GPI
Beiträge: 1511
Registriert: 29.08.2004 13:18
Kontaktdaten:

Re: GUI & ein(!)Thread

Beitrag von GPI »

Wichtig: Die ganzen Gadget und Windows-Befehle nicht in Thread aufrufen. Das klappt unter Windows, nicht mehr unter Linux/MAC. Verwende aus den Thread ein PostEvent() (das geht) um den "Hauptthread" drauf hinzuweisen, das du was ausgelöst haben möchtest.

Die Semaphore-Befehle solltest du dir anschauen, ist sicher das eine oder andere mal praktisch. Damit kannst du den Thread quasi schlafen (wait) legen und dann durch den Hauptthread aufwecken (Signal).

Ansonsten: Wenn du eine Variable nur in Hauptthread setzt/löscht (wie hier True/False) und in Nebenthread nur abfragst, dann brauchst du keinen Mutex. Das Problem ist immer, wenn ein Wert von verschiedenen Stellen verändert werden kann, dann ist ein Mutex zwingend erforderlich. Besonders wenn gezählt werden soll. Ansonsten rutscht dir ein Thread dazwischen. Gleiches gilt für Strings. Oder wenn du Structuren füllen willst. Hier würde ich ein Mutex benutzen.

Mehrere Threads gleichzeitig, die einen Variable lesen wollen, ist auch kein Problem.

Problematisch sind auch listen und maps. Hier verschiebt man ja immer das zu lesende Element. Hier muss man entweder in dauer des Lese-/Schreibvorgangs immer komplett sperren, oder sich einen "Pointer ziehen" und mit diesen Arbeiten. Solange kein Element gelöscht wird, gibts da keine Probleme.
CodeArchiv Rebirth: Deutsches Forum Github Hilfe ist immer gern gesehen!
Benutzeravatar
Muttonhead
Beiträge: 20
Registriert: 25.06.2017 14:06
Computerausstattung: I7

Re: GUI & ein(!)Thread

Beitrag von Muttonhead »

@GPI:
PostEvent() und nicht im Thread... sind erstmal sehr wichtige Hinweise!!! Danke dir.
Mit dem Verständnis bei Semaphoren.. Ich muss da noch ein wenig herumexperimentieren :)

ob ich es grad so umsetzen kann... erstmal vielen Dank, ich werde mich nochmal melden
Mutton
PureBasic 5.62 (Windows 10 Home - x64) | i7 7700HQ | 32GB | HD Graphics 630 / GeForce GTX 1060 Max-Q
GPI
Beiträge: 1511
Registriert: 29.08.2004 13:18
Kontaktdaten:

Re: GUI & ein(!)Thread

Beitrag von GPI »

Muttonhead hat geschrieben:Mit dem Verständnis bei Semaphoren.. Ich muss da noch ein wenig herumexperimentieren :)
Ist nicht weiter kompliziert ein Wait wartet, bis ein Signal erfolgt ist. Das kann auch "Vorsorglich" erfolgen. Sprich wenn ein Signal schon gegeben wurde, obwohl noch kein Wait erfolgt ist, dann geht das Wait sofort weiter. Genauso wird gezählt. Zwei Signals hintereinander geben zwei Wait-Aufrufe frei.

Technisch muss man das so verstehen: Es gibt einen Zähler, der startet mit 0. Wenn ein Wait kommt, dann veringert er diesen Zähler. Falls er kleiner als Null ist, dann wartet er, bis er Null oder größer wird.
Ein Signal erhöht nur den Zähler um eins. Mehr macht er nicht.

Du kannst bspw. eine Liste mit "Aufgaben" anlegen.
In Thread wartest du mit "wait" bis Daten da sind. Der Hauptthread sperrt per Mutex die Liste, stellt einen Eintrag (Tip, als letzter Eintrag) mit daten ein, enstperrst und gibt ein Signal.
Dadurch wird der Thread freigeben. Der Sperrt die Liste, kopiert Eintrag mit Daten, löscht den Eintrag und gibt die Liste wieder frei. Wichtig nur einen Eintrag rausnehmen, weil für jeden Eintrag gibts ein Signal. verarbeitest die Daten und gelangst dank einer Schleife wieder beim Wait.

Der Gag ist, du kannst theoretisch sogar mehrere Threads mit den gleichen Semaphore "warten" lassen. Dann schnappt sich immer der Thread, der zur Zeit nichts zu tun hat, alles.

Code: Alles auswählen

CompilerIf #PB_Compiler_Thread=#False
  CompilerError "Threadsafe aktivieren! Wegen ausgabe auf Debug in Thread!"
  End
CompilerEndIf


Global NewList Aufgaben()

Global MutexAufgaben=CreateMutex()
Global SemaAufgaben=CreateSemaphore()


Global Dim Activ(4)

Global ti=ElapsedMilliseconds()


Procedure MyThread(number)
  Repeat
    activ(number)=#False
    WaitSemaphore(SemaAufgaben)
    activ(number)=#True
    
    LockMutex(MutexAufgaben)
    LastElement(Aufgaben())
    workwith=Aufgaben()
    DeleteElement(Aufgaben())
    UnlockMutex(MutexAufgaben)
    
    Debug Str(ElapsedMilliseconds()-ti)+":  Thread:"+number+" arbeitet mit "+workwith
    
    Delay(1000)
    Debug Str(ElapsedMilliseconds()-ti)+":  Thread:"+number+" fertig mit "+workwith
    
  ForEver   
EndProcedure

;mal 4 Arbeiter erstellen.
Debug "Starte Threads"
For i=0 To 3
  CreateThread(@MyThread(),i)
Next

Delay(1000)

;wir erstellen mal 20 Aufgaben
For i=1 To 20
  Debug Str(ElapsedMilliseconds()-ti)+":Neue Aufgabe "+i
  LockMutex(MutexAufgaben)
  ResetList( Aufgaben() )
  AddElement( Aufgaben() )
  Aufgaben() = i
  UnlockMutex(MutexAufgaben)
  SignalSemaphore(SemaAufgaben)
  Delay(200)
Next

;wir warten hier mal auf das abarbeiten
Debug Str(ElapsedMilliseconds()-ti)+":warte auf Aufgaben-ende"
While ListSize( Aufgaben() )>0 ; Da nicht berechnet und es das aktuelle Element nicht verändert, geht das ohne Mutex!
  Delay(100)
Wend

;Jetzt ist es noch möglich, das ein Thread activ ist
Debug Str(ElapsedMilliseconds()-ti)+":warte auf threadende"
While Activ(0) Or activ(1) Or Activ(2) Or activ(3)
  Delay(100)
Wend

Debug Str(ElapsedMilliseconds()-ti)+":FERTIG"
Delay(2000)


Hier werden mal 20 Aufgaben verteilt, die in 200ms-Tackt anfallen und für die Bearbeitung 1000 Sekunden braucht. Dafür werden 4 Threads angelegt. Die Aufgaben werden immer an den Freien weitergegeben.
CodeArchiv Rebirth: Deutsches Forum Github Hilfe ist immer gern gesehen!
Benutzeravatar
Muttonhead
Beiträge: 20
Registriert: 25.06.2017 14:06
Computerausstattung: I7

Re: GUI & ein(!)Thread

Beitrag von Muttonhead »

Ok, ich glaub es sogar verstanden zu haben :)
Kurze Frage zum "Aufräumen":
nach erfolgreichem Verlassen dieser While...Wend hier

Code: Alles auswählen

;Jetzt ist es noch möglich, das ein Thread activ ist
Debug Str(ElapsedMilliseconds()-ti)+":warte auf threadende"
While Activ(0) Or activ(1) Or Activ(2) Or activ(3)
  Delay(100)
Wend 

sind die Threads ja nicht beendet, sie warten alle auf nen Signal der Semaphore, ist das richtig?
Werden Threads beim Ende des Hauptprogramms auf beendet? Hab jetzt mal nicht RTFM betrieben :D

Mutton
PureBasic 5.62 (Windows 10 Home - x64) | i7 7700HQ | 32GB | HD Graphics 630 / GeForce GTX 1060 Max-Q
Benutzeravatar
ts-soft
Beiträge: 22292
Registriert: 08.09.2004 00:57
Computerausstattung: Mainboard: MSI 970A-G43
CPU: AMD FX-6300 Six-Core Processor
GraKa: GeForce GTX 750 Ti, 2 GB
Memory: 16 GB DDR3-1600 - Dual Channel
Wohnort: Berlin

Re: GUI & ein(!)Thread

Beitrag von ts-soft »

Werden Threads beim Ende des Hauptprogramms auf beendet?
Ja, abrupt! Das heißt, die können nicht aufräumen.

Also, immer zum Schluß den Threads ein Signal zum beenden geben und warten, bis diese beendet sind (bzw. Timeout erreicht).
PureBasic 5.73 LTS | SpiderBasic 2.30 | Windows 10 Pro (x64) | Linux Mint 20.1 (x64)
Nutella hat nur sehr wenig Vitamine. Deswegen muss man davon relativ viel essen.
Bild
Benutzeravatar
mk-soft
Beiträge: 3695
Registriert: 24.11.2004 13:12
Wohnort: Germany

Re: GUI & ein(!)Thread

Beitrag von mk-soft »

Thread werden abgeschossen. Dieses für zu undefinierten zustände. Etwas mehr aufwand sollte man mit Threads zu durchführen...

Kann ja mal ein Beispiel bauen.

Fertig! (Update v0.2)

Code: Alles auswählen

;-TOP

CompilerIf Not #PB_Compiler_Thread
  CompilerError "Option Threadsafe einschalten!"
CompilerEndIf

; Threads und GUI

; Thread Events

Enumeration #PB_Event_FirstCustomValue
  #My_Event_Text
EndEnumeration

; Hilfsfuntion Texte an GUI senden
Procedure AllocateString(Text.s)
  Protected *mem.String
  *mem = AllocateStructure(String)
  If *mem
    *mem\s = Text
  EndIf
  ProcedureReturn *mem
EndProcedure

Procedure.s FreeString(*Mem.String)
  Protected Text.s
  If *Mem
    text = *Mem\s
    FreeStructure(*Mem)
  EndIf
  ProcedureReturn text
EndProcedure

; Basisstruktur für Threads

Structure udtMyData
  ; Basis
  ThreadID.i
  Exit.i
  Semaphore.i
  ; Daten
  Text.s
  Wert.i
EndStructure

; Thread

Procedure thMyThread(*MyData.udtMyData)
  Protected text.s, *mem
  With *MyData
    ; Init Thread
    If Not \Semaphore
      \Semaphore = CreateSemaphore()
    EndIf
    \Text = "Thread " + Str(\ThreadID) + ": "
    ; Thread Loop
    Repeat
      ; Auf Signal warten
      WaitSemaphore(\Semaphore)
      If \Exit
        Break ; Thread loop verlassen
      EndIf
      \Wert + 1
      text = \Text + "Trigger " + \Wert
      *mem = AllocateString(text)
      PostEvent(#My_Event_Text, 0, 0, 0, *mem)
    ForEver
    ; Release Thread
    If \Semaphore
      FreeSemaphore(\Semaphore)
      \Semaphore = 0
    EndIf
    \Exit = 0
  EndWith
EndProcedure

; GUI

Enumeration Window
  #Main
EndEnumeration

Enumeration Gadget
  #List
  #Button
EndEnumeration

Global MyThread1.udtMyData
Global MyThread2.udtMyData

Procedure Main()
  Protected event, text.s, *mem
  
  If OpenWindow(#Main, #PB_Ignore, #PB_Ignore, 400, 340, "Treads und GUI", #PB_Window_SystemMenu)
    ListViewGadget(#List, 5, 5, 390, 300)
    ButtonGadget(#Button, 5, 310, 120, 25, "Trigger")
    
    ; Thread erstellen 
    MyThread1\ThreadID = CreateThread(@thMyThread(), @MyThread1)
    MyThread2\ThreadID = CreateThread(@thMyThread(), @MyThread2)
    
    Repeat
      event = WaitWindowEvent()
      Select event
        Case #PB_Event_CloseWindow
          Break
        Case #PB_Event_Gadget
          Select EventGadget()
            Case #Button
              If MyThread1\Semaphore
                SignalSemaphore(MyThread1\Semaphore)
              EndIf
              If MyThread2\Semaphore
                SignalSemaphore(MyThread2\Semaphore)
              EndIf
          EndSelect
        Case #My_Event_Text
          *mem = EventData()
          text = FreeString(*mem)
          AddGadgetItem(#List, -1, text)
      EndSelect
      
    ForEver
    
    ; Threads beenden
    If MyThread1\Semaphore
      MyThread1\Exit = 1
      SignalSemaphore(MyThread1\Semaphore)
    EndIf
    
    If MyThread2\Semaphore
      MyThread2\Exit = 1
      SignalSemaphore(MyThread2\Semaphore)
    EndIf
    
    If WaitThread(MyThread1\ThreadID, 500) = 0
      KillThread(MyThread1\ThreadID)
      Debug "Thread killed!"
    EndIf
    If WaitThread(MyThread2\ThreadID, 500) = 0
      KillThread(MyThread2\ThreadID)
      Debug "Thread killed!"
    EndIf
    
  EndIf
  
EndProcedure : Main()
Alles ist möglich, fragt sich nur wie...
Projekte ThreadToGUI / EventDesigner V3 / OOP-BaseClass-Modul
Downloads auf MyWebspace / OneDrive
GPI
Beiträge: 1511
Registriert: 29.08.2004 13:18
Kontaktdaten:

Re: GUI & ein(!)Thread

Beitrag von GPI »

Muttonhead hat geschrieben:sind die Threads ja nicht beendet, sie warten alle auf nen Signal der Semaphore, ist das richtig?
Werden Threads beim Ende des Hauptprogramms auf beendet? Hab jetzt mal nicht RTFM betrieben :D
Jup, du müsstest ihn eine Arbeit "Beenden" schicken (genauer gesagt 4x, da jeder einen abarbeiten muss). In den Fall (und nur in den Fall) wäre es relativ egal, weil die Threads nichts aufräumen müssen. Ich überprüfe in Beispiel nur, ob die Threads gerade was machen (activ-Feld).
CodeArchiv Rebirth: Deutsches Forum Github Hilfe ist immer gern gesehen!
Antworten