Page 1 of 1

Statemachine vs. Thread

Posted: Thu Dec 29, 2022 5:23 pm
by Axolotl
Hey folks!
Very often we see questions here about the surface no longer/not responding fast enough.
Simply writing, this is because the necessary main loop, in which all messages must/should be processed, is not called. (Also the use of BindEvent() needs a main loop.)
A frequently given answer here is called thread.
From my point of view there is also the possibility to split longer lasting tasks and to implement them via a so called statemachine in connection with a timer.
Like everything in life this approach has not only advantages but also some disadvantages. But of course this is also true for threads.
Here is a small windows app, which shows the usage of a Timer driven statemachine.

Bear in mind: I use the scan directory procedure as an example. (I know there are much better implementations around.)
I needed some long lasting job here...

You can abort the scan by pressing the "Abort" button any time.

HINT: Activate the filling of the gadget in slow motion by setting the #SLOW_MOTION to 1 ....

Code: Select all

#SLOW_MOTION = 0  ; == 1 sets timer to 500 ms, very slow gadget filling 

#MainCaption$ = "Test V0.00" 
#Caption$     = "Test " 

Enumeration EWindow 
  #WINDOW_Main 
EndEnumeration 

Enumeration EGadget 
  #GADGET_StrBaseDir 
  #GADGET_BtnUpdate
  #GADGET_BtnAbort 
  #GADGET_LstFiles 
EndEnumeration 
Enumeration ETimer 
  #TIMER_Event 
EndEnumeration 

Enumeration EEvent #PB_Event_FirstCustomValue
  #EVENT_UpdateUI  
EndEnumeration 
; ---------------------------------------------------------------------------------------------------------------------
Structure TFileNames 
  FileName$ 
  Size.i  ; could be .q but on Win64 it is the same. 
  DateCreated.i  
  DateModified.i  
  DateAccessed.i 
EndStructure 
; 
Global NewList FileNames.TFileNames()  

Procedure.i ScanDirectory(Gadget, Directory$, statemachine)  ; Gloabl FileNames() 
  Static DIR = 0 
  Protected fn$, tmp$, ok

  If statemachine = 0  ; ............................................. ; == start scanning file directory 
    DIR = ExamineDirectory(#PB_Any, Directory$, "*.*") 
    If DIR 
      ClearList(FileNames())  
      statemachine + 1  ; start state machine .. state == scan dir 
    EndIf 

  ElseIf statemachine = 1  ; ......................................... ; busy_state == scan file directory 
    If NextDirectoryEntry(DIR) 
      Select DirectoryEntryType(DIR) 
      ; Case #PB_DirectoryEntry_Directory 
      ;   If Len(DirectoryEntryName(DIR)) > 2  ; more than C: 
      ;     ;do some on the subdirectory (not yet) 
      ;   EndIf 
        Case #PB_DirectoryEntry_File
          Debug "TIMER statemachine = 1 .. found " + DirectoryEntryName(DIR) 

          AddElement(FileNames())  
         ;FileNames()\FileName$ = Directory$ + DirectoryEntryName(DIR) 
          FileNames()\FileName$ = DirectoryEntryName(DIR) 
          FileNames()\Size      = DirectoryEntrySize(DIR) 
          FileNames()\DateCreated  = DirectoryEntryDate(DIR, #PB_Date_Created) 
          FileNames()\DateModified = DirectoryEntryDate(DIR, #PB_Date_Modified) 
          FileNames()\DateAccessed = DirectoryEntryDate(DIR, #PB_Date_Accessed) 

      EndSelect ; 
    Else  ; NextDirectoryEntry(DIR) 
      Debug "TIMER statemachine = 1 .. last entry found "+#LF$  
      FinishDirectory(DIR) 
      DIR = 0 

      ResetList(FileNames())  
      ClearGadgetItems(Gadget) 
      statemachine + 1   ; do the next step 
    EndIf 

  ElseIf statemachine = 2  ; ......................................... ; == check on data 
    If NextElement(FileNames()) 
      Debug "TIMER statemachine = 2 .. analyse '" + FileNames()\FileName$ + "'"  

      With FileNames() 
        AddGadgetItem(Gadget, -1, \FileName$ + #LF$ + \Size + #LF$ + 
                      FormatDate("%yyyy.%mm.%dd %hh:%ii:%ss", \DateCreated) + #LF$ + 
                      FormatDate("%yyyy.%mm.%dd %hh:%ii:%ss", \DateModified) + #LF$ + 
                      FormatDate("%yyyy.%mm.%dd %hh:%ii:%ss", \DateAccessed)) 
      EndWith 

    Else 
      ; clean up properly 
      statemachine = 0  ; == done 
    EndIf ; NextElement(FileNames()) 

  Else  ; ............................................................ ; == reset state, unknown state (like user abort)  
    Debug "TIMER statemachine = reset " 

    If DIR <> 0 
      FinishDirectory(DIR) 
      DIR = 0 
    EndIf 
    statemachine = 0   ; reset state 
  EndIf 
  ProcedureReturn statemachine 
EndProcedure 

; ---== UI Section ==--------------------------------------------------------------------------------------------------

Procedure OpenMainWindow(WndW=800, WndH=600)  ; 
  If OpenWindow(#WINDOW_Main, 8, 8, WndW, WndH, #MainCaption$, #PB_Window_SystemMenu | #PB_Window_SizeGadget | #PB_Window_ScreenCentered) 
    StickyWindow(#WINDOW_Main, 1)  ; always on top, my sreen is a mess :) 

    StringGadget(#GADGET_StrBaseDir, 4, 4, WndW-92, 20, "<base path>") 
    GadgetToolTip(#GADGET_StrBaseDir, "Directory with JPEG files which includes Exif data ...") 

    ButtonGadget(#GADGET_BtnUpdate, WndW-84, 4, 80, 24, "Update") 
    GadgetToolTip(#GADGET_BtnUpdate, "Scan the directory for files and data ...") 

    ButtonGadget(#GADGET_BtnAbort, WndW-84, 4, 80, 24, "Abort") 
    GadgetToolTip(#GADGET_BtnAbort, "abort the running scan ...") 
    HideGadget(#GADGET_BtnAbort, 1)  ; #GADGET_BtnAbort or #GADGET_BtnUpdate only one is visible :) 

    ListIconGadget(#GADGET_LstFiles, 4, 32, WndW-8, WndH-40, "FileName", 240, #PB_ListIcon_FullRowSelect | #PB_ListIcon_AlwaysShowSelection | #PB_ListIcon_GridLines) 
    AddGadgetColumn(#GADGET_LstFiles, 1, "Size", 120)  
    AddGadgetColumn(#GADGET_LstFiles, 2, "Created", 120)  
    AddGadgetColumn(#GADGET_LstFiles, 3, "Modified", 120)  
    AddGadgetColumn(#GADGET_LstFiles, 4, "Accessed", 120)  

    ProcedureReturn 1  ; success 
  EndIf 
  ProcedureReturn 0  ; failure 
EndProcedure

; ---== main program ==------------------------------------------------------------------------------------------------

Procedure main() 
  Protected dir$, busy_state, tmp 
  Protected DIR 

  If OpenMainWindow() 
    busy_state = 0  
    dir$ = GetTemporaryDirectory() 
    SetGadgetText(#GADGET_StrBaseDir, dir$) 

    Repeat  ; <--- main loop ---> 
      Select WaitWindowEvent() 
        Case #PB_Event_CloseWindow 
          Select EventWindow() 
            Case #WINDOW_Main 
              If busy_state = 0 
                Break   ; say bye 
              Else 
                MessageRequester(#Caption$, "I am still busy!" + #LF$ + "Try again later...." , #PB_MessageRequester_Ok) 
              EndIf 
          ; Case #WINDOW_View 
          ;   HideWindow(#WINDOW_View, 1) 
          EndSelect ; EventWindow() 
        Case #EVENT_UpdateUI  ; my own event :)  
          tmp = EventData() 
          HideGadget(#GADGET_BtnUpdate, tmp)  
          HideGadget(#GADGET_BtnAbort, 1-tmp) 
        Case #PB_Event_Timer 
          Select EventTimer() 
            Case #TIMER_Event  ; running the state machine by timer calls 
              busy_state = ScanDirectory(#GADGET_LstFiles, dir$, busy_state)  ; busy_state changed inside procedure 
              If busy_state = 0 
                RemoveWindowTimer(#WINDOW_Main, #TIMER_Event) 
                PostEvent(#EVENT_UpdateUI, 0, 0, 0, 0) 
              EndIf 
CompilerIf #SLOW_MOTION ; #PB_Compiler_Debugger ; #### 
              If busy_state = 2  ; second part of the statemachine (analysing) in slow motion :) 
                RemoveWindowTimer(#WINDOW_Main, #TIMER_Event) 
                AddWindowTimer(#WINDOW_Main, #TIMER_Event, 500)  ; slower to deal with the program 
              EndIf 
CompilerEndIf 
          EndSelect ; EventTimer() 
        Case #PB_Event_Gadget 
          Select EventGadget() 
            Case #GADGET_BtnUpdate 
              dir$ = GetGadgetText(#GADGET_StrBaseDir) 
              busy_state = ScanDirectory(#GADGET_LstFiles, dir$, 0)  ; start, busy_state changed inside procedure 
              If busy_state 
                AddWindowTimer(#WINDOW_Main, #TIMER_Event, #USER_TIMER_MINIMUM)  ; as fast as we can :)  == 10 ms 
              EndIf 
              ClearGadgetItems(#GADGET_LstFiles) 
              AddGadgetItem(#GADGET_LstFiles, -1, "Scanning... ") 
              PostEvent(#EVENT_UpdateUI, 0, 0, 0, 1) 
            Case #GADGET_BtnAbort        :Debug "abort the Update of the Image File List ... " 
              If busy_state 
                busy_state = -1  ; end the statemachine by next timer call :) 
              EndIf 
              AddGadgetItem(#GADGET_LstFiles, -1, "User Abort") 
              PostEvent(#EVENT_UpdateUI, 0, 0, 0, 0) 
          EndSelect 
      EndSelect 
    ForEver 
    RemoveWindowTimer(#WINDOW_Main, #TIMER_Event) 
  EndIf 
  ProcedureReturn 0 
EndProcedure 

End main() 

Re: Statemachine vs. Thread

Posted: Thu Dec 29, 2022 5:28 pm
by Axolotl
Perhaps you are interested in the comparison of Timeout, Timer and Thread...

Here is a small windows app, which shows the different behavior between Timeout, Timer and Thread.

Code: Select all

; File : Test_TimerOrThread.pb  
; Purpose : Show the differnce between EventTimeout, Timer and Threads implementation. 
;           based on an example of the Purebasic Help 
; Licence : You can do whatever you want to do. (no warranty) 
; 

CompilerIf Not #PB_Compiler_Thread 
  CompilerError "Compiler Options _ Create threadsafe executable must be checked. " 
  ; >> HINT : We need this for StartDrawing(), StopDrawing() 
  ; >> 
  ; >> See Remarks on StartDrawing() 
  ; >>  If "Create thread-safe executable" is enabled in the compiler options then every thread has its own 
  ; >>  current drawing output, which means two threads can do drawing on separate outputs at the same time. 
  ; >> 
CompilerEndIf 

#ProgramName$          = "Test_TimerOrThread" 
#ProgramVersion$       = "0." + #PB_Editor_BuildCount + "." + #PB_Editor_CompileCount 
#MainCaption$          = "Difference between Timer and Threads (EXPERIMENTAL) V" + #ProgramVersion$ 


Enumeration EWindow 
  #WINDOW_Main 
EndEnumeration

Enumeration EGadget 
  #GADGET_LblSettings 
  #GADGET_TrkSpeed 
  #GADGET_TrkOffset 
  #GADGET_LblDescription 

  #GADGET_ImgOne 
  #GADGET_ImgTwo 
  #GADGET_ImgThree 

  #GADGET_BtnResetCount 
  #GADGET_BtnPauseThread  
  #GADGET_BtnResumeThread 
EndEnumeration 

Enumeration EMenu 
  #MENU_Main 
EndEnumeration 

Enumeration EMenuItem  
  #MENU_Close 
EndEnumeration 

Enumeration EImage 1 
  #IMAGE_OnTimeout 
  #IMAGE_OnTimer 
  #IMAGE_OnThread 
EndEnumeration

Enumeration ETimer 
  #TIMER_Event 
EndEnumeration


Global AppExit = #False 
Global Speed = 10, Offset = 4 
Global Dim Counter(2)  ; 

Procedure PaintImageOne(Gadget, Image) 
  Static x = 50, y = 50, dx, dy, cnt = 0  

  If dx = 0 : dx = Offset : EndIf : If dy = 0 : dy = Offset : EndIf 
  x + dx : If x > 250 : dx = -Offset : EndIf : If x < 50 : dx = Offset : EndIf 
  y + dy : If y > 150 : dy = -Offset : EndIf : If y < 50 : dy = Offset : EndIf 

  If CreateImage(Image, 300, 200) 
    If StartDrawing(ImageOutput(Image))
        Circle(x, y, 50, RGB(0,0,255)) 
        Box(150,20,20,20, RGB(0,255,0)) 
        FrontColor(RGB(255,0,0)) 
        For k=0 To 20
          LineXY(10,10+k*8,200, 0)
        Next
        DrawingMode(#PB_2DDrawing_Transparent)
        BackColor(RGB(0,155,155)) 
        FrontColor(RGB(255,255,255))
        DrawText(10, 50, "Image #" + Image + " by event timeout " + Speed + " ms ") 
        Counter(0) + 1 : DrawText(10, 68, "Count: " + Counter(0)) 
      StopDrawing()
    EndIf
  EndIf
  SetGadgetState(Gadget, ImageID(Image)) 
EndProcedure 

Procedure PaintImageTwo(Gadget, Image) 
  Static x = 50, y = 50, dx, dy, cnt = 0  

  If dx = 0 : dx = Offset : EndIf : If dy = 0 : dy = Offset : EndIf 
  x + dx : If x > 250 : dx = -Offset : EndIf : If x < 50 : dx = Offset : EndIf 
  y + dy : If y > 150 : dy = -Offset : EndIf : If y < 50 : dy = Offset : EndIf 

  If CreateImage(Image, 300, 200) 
    If StartDrawing(ImageOutput(Image))
      Circle(x, y, 50, RGB(0,0,255)) 
      Box(150,20,20,20, RGB(0,255,0)) 
      FrontColor(RGB(255,0,0)) 
      For k=0 To 20
        LineXY(10,10+k*8,200, 0)
      Next
      DrawingMode(#PB_2DDrawing_Transparent)
      BackColor(RGB(0,155,155)) 
      FrontColor(RGB(255,255,255))
      DrawText(10, 50, "Image #" + Image + " by timer " + Speed + " ms ") 
      Counter(1) + 1 : DrawText(10, 68, "Count: " + Counter(1)) 
      StopDrawing()
    EndIf
  EndIf
  SetGadgetState(Gadget, ImageID(Image)) 
EndProcedure 

Procedure PaintImageThree(Gadget, Image) 
  Static x = 50, y = 50, dx, dy 

  If dx = 0 : dx = Offset : EndIf : If dy = 0 : dy = Offset : EndIf 
  x + dx : If x > 250 : dx = -Offset : EndIf : If x < 50 : dx = Offset : EndIf 
  y + dy : If y > 150 : dy = -Offset : EndIf : If y < 50 : dy = Offset : EndIf 

  If CreateImage(Image, 300, 200) 
    If StartDrawing(ImageOutput(Image))
      Circle(x, y, 50, RGB(0,0,255)) 
      Box(150,20,20,20, RGB(0,255,0))
      FrontColor(RGB(255,0,0)) 
      For k=0 To 20
        LineXY(10,10+k*8,200, 0)
      Next
      DrawingMode(#PB_2DDrawing_Transparent)
      BackColor(RGB(0,155,155)) 
      FrontColor(RGB(255,255,255))
      DrawText(10, 50, "Image #" + Image + " by thread (delay= " + Speed + " ms)") 
      Counter(2) + 1 : DrawText(10, 68, "Count: " + Counter(2)) 
      StopDrawing()
    EndIf
  EndIf
  SetGadgetState(Gadget, ImageID(Image)) 
EndProcedure 

Procedure PaintByThread(*Value) 
  Repeat
    PaintImageThree(#GADGET_ImgThree, #IMAGE_OnThread) 
    Delay(Speed)  
    If AppExit = #True : Break : EndIf 
  ForEver 
EndProcedure 

Procedure UpdateWindowTimer(Window, Timer, Timeout) 
  RemoveWindowTimer(Window, Timer) 
  AddWindowTimer(Window, Timer, Timeout) 
EndProcedure 

Procedure main() 
  Protected thread, cnt 

  If OpenWindow(#WINDOW_Main, 0, 0, 620, 470, #MainCaption$, #PB_Window_SystemMenu|#PB_Window_ScreenCentered) 
    StickyWindow(#WINDOW_Main, 1) 
    If CreateMenu(#MENU_Main, WindowID(#WINDOW_Main)) 
      MenuTitle("File")
        MenuBar()
        MenuItem(#MENU_Close, "Close or Quit") 
    EndIf 
    TextGadget(#GADGET_LblSettings,   10, 10, 250, 20, "Set Speed = " + Speed + ", Offset = " + Offset) 
    TrackBarGadget(#GADGET_TrkSpeed,  10, 40, 120, 20, 1, 20) 
    TrackBarGadget(#GADGET_TrkOffset, 140, 40, 120, 20, 1, 5) 
    SetGadgetState(#GADGET_TrkSpeed, Speed) 
    SetGadgetState(#GADGET_TrkOffset, Offset) 

    TextGadget(#GADGET_LblDescription, 10, 70, 280, 80, 
               "Open the (System) Menu or " + #LF$ + 
               "Click on Windows Titlebar and " + #LF$ + 
               "hold mouse button down or " + #LF$ + 
               "Move Window around to see a different behavior ") 
; 
    ButtonGadget(#GADGET_BtnResetCount, 10, 420, 140, 24, "Reset Counters") 
    ButtonGadget(#GADGET_BtnPauseThread,  315, 420, 140, 24, "Pause Thread") 
    ButtonGadget(#GADGET_BtnResumeThread, 465, 420, 140, 24, "Resume Thread") 

; >> Create the gadgets to display our nice images
    ImageGadget(#GADGET_ImgOne,   315,   5, 300, 200, 0) 
    ImageGadget(#GADGET_ImgTwo,     5, 210, 300, 200, 0) 
    ImageGadget(#GADGET_ImgThree, 315, 210, 300, 200, 0) 

; >> Timer and Thread 
    AddWindowTimer(#WINDOW_Main, #TIMER_Event, Speed)  ; on windows  #USER_TIMER_MINIMUM == 10 ms 
    thread = CreateThread(@PaintByThread(), 0) 
    DisableGadget(#GADGET_BtnResumeThread, 1) 

    Repeat 
      Select WaitWindowEvent(Speed) 
        Case #PB_Event_None 
          PaintImageOne(#GADGET_ImgOne, #IMAGE_OnTimeout) 

        Case #PB_Event_Timer 
          PaintImageTwo(#GADGET_ImgTwo, #IMAGE_OnTimer) 

        Case #PB_Event_CloseWindow  ; If the user has pressed on the window close button 
         ;If EventWindow() = #WINDOW_Main  ; <-- not needed, only one window :) 
          AppExit = #True 
          If IsThread(thread) And WaitThread(thread, 1000) = 0  
            MessageRequester("INFO", " The thread could't be finished properly yet, probably you should try again") ; I am so funny! 
            If WaitThread(thread, 1000) <> 0   ; usally this ends. The message helped as a user response. 
              Break  ; okay, done. 
            EndIf 
          Else 
            Break  ; say good bye. 
          EndIf 

        Case #PB_Event_Menu 
          Select EventMenu() 
            Case #MENU_Close 
              PostEvent(#PB_Event_CloseWindow)  ; leave the loop -- not by break 
          EndSelect 

        Case #PB_Event_Gadget 
          Select EventGadget() 
            Case #GADGET_TrkSpeed 
              Speed = GetGadgetState(#GADGET_TrkSpeed) 
              SetGadgetText(#GADGET_LblSettings, "Set Speed = " + Speed + ", Offset = " + Offset) 
              UpdateWindowTimer(#WINDOW_Main, #TIMER_Event, Speed) 

            Case #GADGET_TrkOffset 
              Offset = GetGadgetState(#GADGET_TrkOffset) 
              SetGadgetText(#GADGET_LblSettings, "Set Speed = " + Speed + ", Offset = " + Offset) 

            Case #GADGET_BtnResetCount 
              Counter(0) = 0 : Counter(1) = 0 : Counter(2) = 0 

            Case #GADGET_BtnPauseThread 
              If IsThread(thread) 
                PauseThread(thread) 
                DisableGadget(#GADGET_BtnPauseThread, 1) 
                DisableGadget(#GADGET_BtnResumeThread, 0) 
              EndIf 

            Case #GADGET_BtnResumeThread 
              If IsThread(thread) 
                ResumeThread(thread) 
                DisableGadget(#GADGET_BtnResumeThread, 1) 
                DisableGadget(#GADGET_BtnPauseThread, 0) 
              EndIf 

          EndSelect 
      EndSelect 
    ForEver 
  EndIf
EndProcedure 
main() 

Re: Statemachine vs. Thread

Posted: Thu Dec 29, 2022 11:53 pm
by Cezary
Nice explanation.
But what about this case:

Code: Select all

If OpenWindow(0, 0, 0, 400, 200, "Timer Example", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  ProgressBarGadget(0, 10, 10, 380, 20, 0, 100)
  ButtonGadget(1, 180, 150, 40, 20, "Click me")
  AddWindowTimer(0, 123, 250)
  
  Value = 0
  Repeat
    Event = WaitWindowEvent()
    
    If Event = #PB_Event_Timer And EventTimer() = 123
      Value = (Value + 5) % 100
      SetGadgetState(0, Value)
    ElseIf Event = #PB_Event_Gadget
      MessageRequester("Test", "What now?", #PB_MessageRequester_Ok)
    EndIf    
    
  Until Event = #PB_Event_CloseWindow
EndIf



Re: Statemachine vs. Thread

Posted: Sat Dec 31, 2022 12:47 pm
by Axolotl
Cezary wrote: Thu Dec 29, 2022 11:53 pm Nice explanation.
But what about this case:

Code: Select all

; code above
Thanks.
Yes, that is one of the disadvantages...
You/your app always have/has to go through the main loop.
By situations like messagebox, menu, window-titlebar-clicks the main loop is no longer operated by the (windows) 'system'...
If one terminates the e.g. messagebox, it goes however further...
I - for my part - use menus and messageboxes less often, so for me the statemachine solution is well suited.
But also not always, then I also fall back on threads.

To be honest: The idea behind my post was to get the reader (as a beginner) more involved with the basic method of messaging.
My point is not to say that a statemachine is better than using threads.

In the end, it's all about how you reach the goal you want to achieve.
Software development offers many possibilities.
That's great.

Happy coding and stay healthy.

Re: Statemachine vs. Thread

Posted: Sat Dec 31, 2022 2:00 pm
by Cezary
Axolotl,
you are right: we need to use the right solution for the specific problem. To be honest - I use state machines very often, especially in the embedded applications where using an OS is not necessary.
It is always worth getting to know a different point of view to learn something, especially in such a complex matter as programming. Therefore, it is worth checking the tips&tricks section of this forum.

Best regards

Re: Statemachine vs. Thread

Posted: Sat Dec 31, 2022 2:17 pm
by Axolotl
Very interesting. I actually started assembler programming without OS on 6805 and 8051 as well.
But that was really a long time ago. Today programming is only a hobby for me.

Happy coding and stay healthy.