This article covers the basics of threading in Small Basic.  This is usually quite an advanced concept and perhaps a surprising discussion point for Small Basic, but it does raise its head in many Small Basic programs.  Therefore, it is worth explaining what it is and how and why to avoid some common pitfalls.

What are Threads?

A thread is a process within a program that can run at the same time as other processes or threads.  This is called asynchronous since multiple threads can be running at the same time.

The threads may run on separate CPUs if you have a multi-core PC or be shared on one CPU.  The operating system will take care of this for you, but you can always think of them running at the same time.

UI Thread

The main thread which is started when a Small Basic program starts and is called the UI (User Interface) thread.  It is responsible for running the main program (everything apart from event subroutines).  It is also responsible for updating what you see (the UI), for example the GraphicsWindow.  When you call a command like Shapes.AddEllipse(20,20), the ellipse object and its properties are created and a message is passed to the UI thread to update what you see on the screen.

Event Threads

When an event is defined in Small Basic, the event is associated with an event subroutine.  The event subroutine is called on a new thread when the event occurs.  Clearly an event like a mouse click can occur at any time and the event subroutine is called asynchronously - in parallel or at the same time as the main program or even another event threads.

Multiple Threads

The program always starts on the main thread, and additional threads are created in Small Basic when event subroutines are called.  When an event subroutine exits, then the event thread is disposed of.  There is therefore a big distinction between code that operates on the main thread and code that is called from events.

It is not the actual subroutines that determine which thread they are on, rather is is how they are called (from the main program or from an event). Therefore, it is possible to call an event subroutine (it is just a subroutine) from the main thread and also call a non event subroutine from an event.

RingBell()
GraphicsWindow.MouseDown = OnMouseDown
 
Sub OnMouseDown
  RingBell()
EndSub
 
Sub RingBell
  Sound.PlayBellRing()
EndSub

In the code above, OnMouseDown is an event subroutine and RingBell is a 'normal' subroutine.  RingBell is called at the start on the main thread and it is then called by OnMouseDown on the event thread each time the mouse button is pressed.  So whether Sound.PlayBellRing() is called on the main or the event thread depends on how it was called.

What are the issues with threads in Small Basic?

This section lists some of the main issues with threads which are present in all multi-threaded programs and in particular in Small Basic.  We also draw some conclusions about 'best practice' arising from these issues.

Because threading related problems can cause unpredictable behavior, debugging and fixing them can be especially difficult, so it is best to design your code with these ideas in mind.

Global data

All Small Basic variables are global scope.  This means that they can be accessed and changed at any point within a Small Basic program.  As a result, if different threads are using the same variable to do different things at the same time, then unpredictable results may occur.  Be particularly careful not to use the same loop index counters in the main code and event subroutines.

Timer.Interval = 1000
Timer.Tick = OnTick
 
'Main UI thread
i = 0
While ("True")
  i = i+1
  TextWindow.WriteLine("UI Thread "+i)
  Program.Delay(1000)
EndWhile
 
'Timer event thread
Sub OnTick
  For i = 1 To 10
    For j = 1 To 1000
      y[j] = i*j
    EndFor
  EndFor
EndSub

Above is a rather contrived example, but it shows the issue that the variable 'i' is used in the UI and Timer threads and therefore its value is changed by both giving unpredictable results such as those below.

Conclusion - Don't use the same variable names inside code that can be called on different threads unless you want them to be modified and used by both, in which case there is no guarantee exactly when the variables will change.  In Small Basic variables are 'thread safe' so your program won't usually crash, but you may get unexpected behavior.

Re-entrancy

Re-entrancy is where an event thread is called again before it has completed the last event and exited from the event subroutine.  Most Small Basic events are not re-entrant.  For example the following calls the bell each time the mouse is moved and doesn't recall the event until it has finished with the last.  See that the debugging TextWindow output shows we Enter and Leave the event subroutine in order (not re-entrant).  Note that when the mouse moves several events are raised in succession as the mouse moves, but only the last is queued for the event subroutine, resulting one extra bell ring after we have finished moving the mouse.

GraphicsWindow.MouseMove = OnMouseMove
 
Sub OnMouseMove
  TextWindow.WriteLine("Enter")
  Sound.PlayBellRingAndWait()
  TextWindow.WriteLine("Leave")
EndSub

Note however, using the Timer, this is re-entrant with several entries before the first is finished.

Timer.Interval = 100
Timer.Tick = OnTick
 
Sub OnTick
  TextWindow.WriteLine("Enter")
  Sound.PlayBellRingAndWait()
  TextWindow.WriteLine("Leave")
EndSub

If we stop the Timer after 2 calls, we see that many more bell rings (around 20) are made before the bell ringing actually stops.  In this case the Timer event calls are queued to be called before the timer is paused.

Timer.Interval = 100
Timer.Tick = OnTick
count = 0
 
Sub OnTick
  count = count+1
  If (count > 2) Then
    Timer.Pause()
  EndIf
  TextWindow.WriteLine("Enter "+count)
  Sound.PlayBellRingAndWait()
  TextWindow.WriteLine("Leave "+count)
EndSub

Conclusion - Re-entrancy is complicated and can be unpredictable.  Do not do so much in the event code that it is likely to take longer to perform than the frequency at which the event may be called.  The best approach is to just set a flag inside the event subroutine and 'do the work' on the main thread. 

Below is an example of this for the Timer event and the results are more predictable.

count = 0
timerTick = "False"
Timer.Interval = 100
Timer.Tick = OnTick
 
While ("True")
  If (timerTick) Then
    timerTick = "False"
    count = count+1
    If (count > 2) Then
      Timer.Pause()
    EndIf
    TextWindow.WriteLine("Enter "+count)
    Sound.PlayBellRingAndWait()
    TextWindow.WriteLine("Leave "+count)
  EndIf
  Program.Delay(20)
EndWhile
 
Sub OnTick
  timerTick = "True"
EndSub

UI latency

As stated above, the main thread also updates the UI.  If we do a lot of update work in an event thread that takes some time, the UI is not updated until the event subroutine finishes.

GraphicsWindow.MouseMove = OnMouseMove
 
Sub OnMouseMove
  TextWindow.WriteLine("Enter")
  ball = Shapes.AddEllipse(50,50)
  Shapes.Move(ball,GraphicsWindow.MouseX-25,GraphicsWindow.MouseY-25)
  Sound.PlayBellRingAndWait()
  TextWindow.WriteLine("Leave")
EndSub

In the example above the ball is not drawn until the event subroutine is finished.  While the subroutine is not re-entrant (see the TextWindow Enter and Leave debugging), the mouse move events are queued and continue to be called long after we have finished moving the mouse.  This is because the UI is updated from the last ball created and the new mouse move may change something so it is queued for calling.

If we delete the bell ringing (added to slow the event subroutine) we actually get a crash and the TextWindow debugging suggests that there may be some re-entrancy, perhaps the ball variable is being replaced before it is finished with.  There are no easy solutions here if we do the graphics (UI work) inside the event subroutine.

Conclusion - Don't do UI work inside an event subroutine, do it on the main UI thread, as shown below. 

mouseMove = "False"
GraphicsWindow.MouseMove = OnMouseMove
 
While ("True")
  If (mouseMove) Then
    mouseMove = "False"
    ball = Shapes.AddEllipse(50,50)
    Shapes.Move(ball,GraphicsWindow.MouseX-25,GraphicsWindow.MouseY-25)
  EndIf
  Program.Delay(20)
EndWhile
 
Sub OnMouseMove
  mouseMove = "True"
EndSub

Note that creating hundreds of ball shapes with each mouse move is a bad idea, it was just done to create short examples.

Timer Thread

As an advanced topic we can actually use the Timer thread to deliberately do something that takes a long time, while also doing normal stuff on the main UI thread.  Bearing in mind the conclusions above:

  1. Be careful not to use any variables like loop counters that are also used in the main program thread.
  2. When the timer event subroutine is called, immediately stop the timer to prevent any re-entrancy, also use a longish timer interval to be sure of this.
  3. Don't do any UI or graphics work on this thread.

As an example, I use a Timer thread to load images for use in a scrolling image viewer - I posted this as a solution to monthly challenge in the light of some discussions on threading and the Timer object in the forum.  This was also the main reason to write this article.  Small Basic import code WFD474.

'Example solution to December 2013 Challenge (Curriculum 2.6(3))
'Uses Timer and Stack to asynchronously download images in preparation for use
'Shape images are created and destroyed as required
'The only limiting feature is that ImageList will continue to grow gradually consuming memory
 
'Setup variables and GW
Initialise()
 
'Start Timer to get images asynchronously (in parallel on Timer thread)
Timer.Interval = 1000
Timer.Tick = OnTick
 
'Main loop
While "True"
  start = Clock.ElapsedMilliseconds
  
  UpdateImages()
  
  'Pause up to 10ms depending on time spent doing updates
  delay = 10 - (Clock.ElapsedMilliseconds-start)
  If (delay > 0) Then
    Program.Delay(delay)
  EndIf
EndWhile
 
'===========================================================
'SUBROUTINES
'===========================================================
 
'All initialisation
Sub Initialise
  imageCount = 0 'Current image number
  imageShape = "" 'Store for current moving images
  tag = "santa claus" 'Tag for Flickr image
  
  'Setup GW
  gw = Desktop.Width-40
  gh = 600
  GraphicsWindow.BackgroundColor = "Black"
  GraphicsWindow.Top = 10
  GraphicsWindow.Left = 10
  GraphicsWindow.Title = tag+" slideshow"
  GraphicsWindow.Height = gh
  GraphicsWindow.Width = gw
  GraphicsWindow.BrushColor = "Pink"
  GraphicsWindow.FontSize = 50
  loading = Shapes.AddText("loading...")
  Shapes.Move(loading,100,100)
  Mouse.HideCursor()
EndSub
 
'Asynchronous download of images - careful not to use any variable names used in main loop subroutines
Sub OnTick
  Timer.Pause() ' Stop the timer as this sub will continue for ever loading images as required
  While ("True")
    If (Stack.GetCount("images") < 10) Then 'load up to 10 images ready to use
      'If we don't use ImageList then we cannot get the image size
      'There is no way in standard SB to remove used images from ImageList so memory will gradually grow
      image = ImageList.LoadImage(Flickr.GetRandomPicture(tag))
      w = ImageList.GetWidthOfImage(image)
      h = ImageList.GetHeightOfImage(image)
      img = Shapes.AddImage(image) 'We can get some flicker when image is added - to minimise we immediately move off-screen
      Shapes.Move(img,-w,(gh-h)/2)
      Stack.PushValue("images", img)
      If (imageShape = "") Then 'Create first image
        NextImage()
        Shapes.Remove(loading)
      EndIf
    EndIf
    Program.Delay(10) 'Don't mash cpu when nothing to do
  EndWhile
EndSub
 
'Get the next image and add it to the moving list array
Sub NextImage
  If (Stack.GetCount("images") > 0) Then
    imageCount = imageCount+1
    imageShape[imageCount] = Stack.PopValue("images")
  EndIf
EndSub
 
'Move the current active images
Sub UpdateImages
  indexShapes = Array.GetAllIndices(imageShape)
  For i = 1 To Array.GetItemCount(indexShapes)
    'Get current moving image and update its position
    index = indexShapes[i]
    shape = imageShape[index]
    x = Shapes.GetLeft(shape)
    y = Shapes.GetTop(shape)
    Shapes.Move(shape, x+1, y)
    'Start a new image (with a pixel space of 10)
    If (x < 10 And x+1 >= 10) Then
      NextImage()
    EndIf
    'Clean up and remove finished images
    If (x > gw) Then
      Shapes.Remove(shape) 'Remove finished with shape
      imageShape[index] = ""
    EndIf
  EndFor
EndSub

See Also

Other Languages