Threading with Squirrel

redfoot.jpg

An example of multi-threading application

Kirilla
Abstract

In this article, we try to demonstrate how using Squirrel to build a multi-threading application can bring simplicity and power.

1. What is a thread ?

Most of the modern operating system like Linux, BeOS or even Windows, offer the possibility of running several functions in paralle. Each of this function is called a thread and will run concurently to the other threads on the computer's CPU. In BeOS each application has the possibility to run as many thread as it need (in a certain limit). The set of threads of an application is called a team. All the threads of a team share the same memory and have access to the same datas. Although very powerfull, this could be dangerous when various threads access a unique ressource.

2. The application

Consider using a BeIA as a device to control and monitor something like a temperature (BeIS is not yet available, but we'll assume it is for the following discussion). It could be the temperature of anything: an oven, a fridge or even a nuclear power plant.

In our application, we going to simulate the monitoring of the exhaust temperature of an airplane engine. We will avoid getting into more detail than necessary for our discussion. Each engine will be controled by three detectors, all of which located in the exhaust pipe. These detectors will provide us the temperature in Farenheiht. Having three detectors instead of one is a security that offer some redundancy.

The rules of the monitor are :

  • All detectors should give the same reading, and if not, there's at least one failing
  • The temperature of the two engines must be within 50F of each other, else a warning will be issued
In the case where the temperature readings are different, the temperature recorder by the majority of the detectors is returned and the faulty detector will have is power recycled (in case the problem could be fixed by a power cycle) and will be read on the next check.

For all abnormal situations, an appropriate message must be displayed.

The user will have control over both the real engines exhaust temperatures and the detectors faults.

3. The User Interface

We going to use two windows for the GUI. One will display the engine's temperatures given by the detectors and the detector's status (correct or fault). The other window will give the user control over the simulation :

gui

The Engine's exhaust monitor window is compose of two elements :

  • A menu bar

    menubar
  • A display area

    display
The menu bar is made by first creating a MenuBar widget and then adding two Menu widgets. It allow the user to start and stop the simulation and to set the automatic Power Recycle of the detectors :
make "menu MenuBar
make "simu Menu "Sim
make "i.start $simu~add "Start {
	$simu~i.enable :i.start false
	$simu~i.enable :i.stop true	
}
make "i.stop $simu~add "Stop {
	$simu~i.enable :i.start true
	$simu~i.enable :i.stop false	
}
$simu~i.enable :i.stop false
$simu~add "separator
$simu~add 'Quit' {
	$sim~quit	
}
make "options Menu "Options
make "recy $options~add 'Recycle detector' {
	if not :Recycle {
		make "Recycle true
		$options~i.mark :recy true				
	} {
		make "Recycle false	
		$options~i.mark :recy false
	}	
}
$menu~add :simu :options
When the user select the item Start from the menu Sim, the item will be disabled and the item Stop will be enabled. When the Stop item is selected, the inverse will occur:

menu1

menu2

The method i.mark allow the marking or unmarking of a menu item. We use this feature for the Options menu to set or unset the automatic recycle of the detectors :

options1

options2

The display area is a Frame that has another Frame and two Banner widgets for its children.

The purpose of the Banners is to display an alert message when something is wrong with the engines, e.g. a detector failure:

make "msg1 Banner "Message1 "center [30 0]
$msg1~config "expand.x "set true
make "msg2 Banner "Message2 "center [30 0]
$msg2~config "expand.x "set true
The widgets size is set to accomodate a string of 30 characters and to justify the text displayed to the center. When updated by the program, the global variable Message1 and Message2 will change the text displayed on the widgets.

message

The engine's temperatures frames are composed of two Box glued side by side :

enginebox and enginebox2

They display both the temperature of the exhaust read by the detectors and the detector's status. The three green squares symbolize the status of the engine's detectors. Each of the squares turns red when a failure of the corresponding detector is .... detected ;)

Both of the boxes are build in the same way :

make "bleft Box 'Engine left'
make "bright Box 'Engine right'

/* the labels in the box */
make "temp.left LabeledText 'Real Temp :' "banner.t.left
make "temp.right LabeledText 'Real Temp :' "banner.t.right
make "det.left Frame
$det.left~config "align.h "set "center
make "det.right Frame
$det.right~config "align.h "set "center

make "e1d1.fail Frame "raised [10 10]
$e1d1.fail~config "bgcolor "set :Green
make "e1d2.fail Frame "raised [10 10]
$e1d2.fail~config "bgcolor "set :Green
make "e1d3.fail Frame "raised [10 10]
$e1d3.fail~config "bgcolor "set :Green

Glue :det.left "left [1 0] :e1d1.fail :e1d2.fail :e1d3.fail

make "e2d1.fail Frame "raised [10 10]
$e2d1.fail~config "bgcolor "set :Green
make "e2d2.fail Frame "raised [10 10]
$e2d2.fail~config "bgcolor "set :Green
make "e2d3.fail Frame "raised [10 10]
$e2d3.fail~config "bgcolor "set :Green

Glue :det.right "left [1 0] :e2d1.fail :e2d2.fail :e2d3.fail

Glue :bleft "top [] :temp.left :det.left
Glue :bright "top [] :temp.right :det.right
To build the temperature display, we use the function LabeledText. This simply creates a Frame with two Text widgets glued :
to LabeledText :text :banner
	make.local "aframe Frame
	$aframe~config "expand.x "set true
	make.local "label Text :text
	make.local "value Banner :banner "right [7 0]
	$value~config "high.color "set :Blue
	Glue :aframe "left [] :label
	Glue :aframe "right [] :value
	output :aframe
end
The window is created with the primitive Window:
make "sim Window "titled 'Engine\'s exhaust monitor' [100 100] "not.zoomable "not.resizable
We also set its hook "quit to destoy the control window when the window is destroyed:
Hook :sim "quit {
	$ctrl~quit	
}
The Control window is composed of two Box widget glued side-by-side:
cbleft and cbright

For each engine, we want to be able to raise or lower the engine exhaust temperature and to simulate a failure on any of the three detectors. Each Box contains two Button widgets and three CheckBox widgets:

make "bleft Box 'Engine left'
make "bright Box 'Engine right'

make "b1 Button 'Raise'
Hook :b1 "invoked {
	incr "engine1.temp 10
}
make "b2 Button 'Lower'
Hook :b2 "invoked {
	incr "engine1.temp -10
	if :engine1.temp < 0 {make "engine1.temp 0}
}
make "d1 CheckBox 'Detector 1' "e1d1
make "d2 CheckBox 'Detector 2' "e1d2
make "d3 CheckBox 'Detector 3' "e1d3
Glue :bleft "top [] :b1 :b2 :d1 :d2 :d3

make "b1 Button 'Raise'
Hook :b1 "invoked {
	incr "engine2.temp 10
}
make "b2 Button 'Lower'
Hook :b2 "invoked {
	incr "engine2.temp -10
	if :engine2.temp < 0 {make "engine1.temp 0}
}
make "d1 CheckBox 'Detector 1' "e2d1
make "d2 CheckBox 'Detector 2' "e2d2
make "d3 CheckBox 'Detector 3' "e2d3
Glue :bright "top [] :b1 :b2 :d1 :d2 :d3
The CheckBox widgets have all their Boolean variables set to true when a detector failure must occurs.

4. The threads

To simulate the engine control system, we going to use one thread for each detector and one thread for each engine. This thread will frequently check the detectors and display the engine's exhaust temperatures as well as any critical messages. A last thread will check the temperature of both engines and display a message when the difference between the two engines is greater than 100F.

Threads in Squirrel are accessible throught the Add-on Threading. The primitive Thread create a thread from a function or a block. The primitive Thread.hop start a thread (or severals threads). Other primitives allow to suspend, kill or wait for the end of a thread

The thread of the detectors will execute the function th.Detector. This function may or may not give the correct exhaust temperature:

to th.Detector :vtemp :vfail :engine 

	while (true) {
		if (thing :vfail) {
			make :vtemp (thing :engine) + (random 10 100)		
		} {
			make :vtemp thing :engine	
		}
		Snooze 700000	
	}
end
The first input vtemp is the name of the global variable containing the value read by the detector. The next input vfail is the name of the variable used to specify wheter or not the detector should fail. The last input is a variable which give acces to the real temperature of the engine as set by the user.

We need to loop forever in order to have this thread going until the simulation is stopped by the user, as the thread end when the function is over. The primitive Snooze will put the thread to sleep for 700000 microseconds.

The function th.Check will be used to check for the engine's exhaust temperature:

to th.Check
	make.local "err false
	while (true) {
		if (abs (:engine1.d - :engine2.d)) <= 100 {
			if not :err {
				make "err true
				Info "stop ["ok] 'Engines not at the same temperature'	
			} 
		} {
			make "err false	
		}
		Snooze 500000		
	}
end
Here, the function loops forever and sleeps each iteration for 500000 microseconds. At each iteration, the difference between the temperatures read between the two engines is calculated. If this difference is significant, we will display an Info message box. The local variable err is used to avoid getting too many message boxes on screen.

The thread cheking the detectors for an engine is the more complex:

to th.Engine :position :e.d :msg :l1 :l2 :l3 :real

local "lock "abnormal "fail1 "fail2 "fail3

/* first we extract the infos on the detector */

local "d1 "d2 "d3 "flag1 "flag2 "flag3 "t1 "t2 "t3 "f1 "f2 "f3

lscan :l1 "d1 "f1 "flag1
lscan :l2 "d2 "f2 "flag2
lscan :l3 "d3 "f3 "flag3

make :msg string 'Monitoring engine ' :position

make "fail1 false
make "fail2 false
make "fail3 false

while (true) {
	
	/* we get the 3 reading of the detectors */
			
	make "t1 thing :d1		
	make "t2 thing :d2
	make "t3 thing :d3	
							
	if (:t1 = :t2) and (:t2 = :t3) {
	
		/* no detector failing */	
	
		make :e.d :t1
		make :real string :t1 ' F'
		if :abnormal {
			make "anbormal false
			make :msg string 'Back to normal for ' :position
			
			/* we need to set back the the failure flag of the detector */
			if :fail1 {
				make "fail1 false
				UpdateFlag :flag1 :Green		
			}
			if :fail2 {
				make "fail2 false
				UpdateFlag :flag2 :Green		
			}
			if :fail3 {
				make "fail3 false
				UpdateFlag :flag3 :Green		
			}	
		}		
	} {
		/* one or more of the detector had a failure */	
		
		if (:t1 <> :t2) and (:t2 <> :t3) and (:t1 <> :t3) {
			/*	all three of the detector are failing (or two of them) */
			make :e.d av :t1 :t2 :t3
			make :real string (thing :e.d) ' F'
			make "abnormal true
			make "fail1 true
			make "fail2 true
			make "fail3 true
			UpdateFlag :flag1 :Red
			UpdateFlag :flag2 :Red
			UpdateFlag :flag3 :Red	
			make :msg string 'Failure of all the detectors on engine ' :position
			if :Recycle {
				make "th1 Thread "normal "th.Recycle :position 1 :f1
				make "th2 Thread "normal "th.Recycle :position 2 :f2
				make "th3 Thread "normal "th.Recycle :position 3 :f3
				Thread.hop :th1 :th2 :th3
			}
		} {	
		
			if (:t1 <> :t2) and (:t1 <> :t3) and (:t2 = :t3) {
				/* 	detector 1 failing	*/
				make :e.d av :t2 :t3
				make :real string (thing :e.d) ' F'
				make :msg string 'Failure of detector 1 on engine ' :position	
				make "abnormal true
				make "fail1 true
				make "fail2 false
				make "fail3 false
				UpdateFlag :flag1 :Red
				UpdateFlag :flag2 :Green
				UpdateFlag :flag3 :Green
				if :Recycle {
					make "th1 Thread "normal "th.Recycle :position 1 :f1
					Thread.hop :th1 			
				}
			} {
			
				if (:t1 <> :t2) and (:t2 <> :t3) and (:t1 = :t3) {
					/* 	detector 2 failing	*/
					make :e.d av :t1 :t3
					make :real string (thing :e.d) ' F'
					make :msg string 'Failure of detector 2 on engine ' :position
					make "abnormal true
					make "fail2 true
					make "fail1 false
					make "fail3 false
					UpdateFlag :flag2 :Red	
					UpdateFlag :flag1 :Green
					UpdateFlag :flag3 :Green
					if :Recycle {
						make "th2 Thread "normal "th.Recycle :position 2 :f2
						Thread.hop :th2 			
					}			
				} {	
					/* 	detector 3 failing	*/
					make :e.d av :t1 :t2
					make :real string (thing :e.d) ' F'
					make :msg string 'Failure of detector 3 on engine ' :position	
					make "abnormal true
					make "fail3 true
					make "fail1 false
					make "fail2 false
					UpdateFlag :flag3 :Red
					UpdateFlag :flag1 :Green
					UpdateFlag :flag2 :Green
					if :Recycle {
						make "th3 Thread "normal "th.Recycle :position 3 :f3
						Thread.hop :th3 			
					}				
				}
			}
		}	
	}
			
	Snooze 400000	
}

end
Once again, the thread is looping forever and sleeping between each iteration. At the beginning of each iteration we read the value of the three detectors and we compare them to see if any are at fault. When a detector is supposed to fail, we set the Frame widget color of the main window representing its status by using the simple function :
to UpdateFlag :flag :color
	$flag~config "bgcolor "set :color 
end
If it is selected, we will simulate the recycling of the power in the detector. To do this, we create a new thread each time and for each failing detectors and we start the thread. It will execute the function th.Recycle:
to th.Recycle :e :n :failure
	/* first we wait for 0.5 second to simulate a recycle */
	Snooze 500000
	/* then we use a random value to see if the widget is failing still or not */
	if (random 0 9) > 5 {
		make :failure false	 	
	}
end
To add more realism a random number determines whether or not the detector is still falling.

Creating the various thread is simply done by calling the primitive Thread:

make "lst.e1d1 list "engine1.d1 "e1d1.failure :e1d1.fail
make "lst.e1d2 list "engine1.d2 "e1d2.failure :e1d2.fail
make "lst.e1d3 list "engine1.d3 "e1d3.failure :e1d3.fail
make "lst.e2d1 list "engine2.d1 "e2d1.failure :e2d1.fail
make "lst.e2d2 list "engine2.d2 "e2d2.failure :e2d2.fail
make "lst.e2d3 list "engine2.d3 "e2d3.failure :e2d3.fail


make "th.e1d1 Thread "normal "th.Detector "engine1.d1 "e1d1.failure "engine1.temp  
make "th.e1d2 Thread "normal "th.Detector "engine1.d2 "e1d2.failure "engine1.temp 
make "th.e1d3 Thread "normal "th.Detector "engine1.d3 "e1d3.failure "engine1.temp
 
make "th.e2d1 Thread "normal "th.Detector "engine2.d1 "e2d1.failure "engine2.temp  
make "th.e2d2 Thread "normal "th.Detector "engine2.d2 "e2d2.failure "engine2.temp 
make "th.e2d3 Thread "normal "th.Detector "engine2.d3 "e2d3.failure "engine2.temp

make "th.engine1 Thread "normal "th.Engine "left "engine1.d "Message1 :lst.e1d1 :lst.e1d2 :lst.e1d3 "banner.t.left
make "th.engine2 Thread "normal "th.Engine "right "engine2.d "Message2 :lst.e2d1 :lst.e2d2 :lst.e2d3  "banner.t.right

make "th.check Thread "normal "th.Check 
All the thread are created with a normal priority. On a real-world application, most of them will have been real-time. Each Thread primitives output a thread id. We will use this id to start the threads when the simulation started. The code executed from the menu must now changed to start or kill the thread:
make "i.start $simu~add "Start {
	$simu~i.enable :i.start false
	$simu~i.enable :i.stop true	
	/* starting the simulation */
	Thread.hop :th.e1d1 :th.e1d2 :th.e1d3 :th.e2d1 :th.e2d2 :th.e2d3
	Thread.hop :th.engine1 :th.engine2	:th.check
}
make "i.stop $simu~add "Stop {
	$simu~i.enable :i.start true
	$simu~i.enable :i.stop false
	Thread.suspend :th.e1d1 :th.e1d2 :th.e1d3 :th.e2d1 :th.e2d2 :th.e2d3
	Thread.suspend :th.engine1 :th.engine2 :th.check	
}
5. Experimenting

When we start the simulation, all the threads start to run:

starting

If we put the detector 1 of the left engine at fault, it is immediatly detected by the thread th.Engine of the left engine and the detector status gets updated. Also a message is displayed:

failure1

If we had set the automatic recycle of the failing detector, we would have gotten:

recycled

In this case, the recycling was succesfull. However it may take longer for the detector to read back the real temperature.

When two detectors are failing, it's impossible to know the exact temperature of the exhaust. In this case, all the detector are marked at fault and their average value is displayed:

failure2

Note that the temperature is changed at each iteration of the thread th.Engine to reflect the change of the detector values.

If we now increase the temperature of the right engine we will get an info message box when the difference is greater than 100F:

difftemp

When the user change the temperature of the engine, it may happend that one of the detector flash it status when it shouldn't:

bug

This is a perfect example of an issue in multi-threading. The user changes the temperature while the thread th.Engine is reading the detectors, and not all the detectors have reflected the change of the temperature of the engine (detectors thread sleeping). To handle this special case, we should have use a Locker object for each detector. We lock the detector when we read or update its value. The reading of the detectors will then have looked like :
		$lk1~lock
		make "t1 thing :d1
		$lk1~unlock
		$lk2~lock		
		make "t2 thing :d2
		$lk2~unlock
		$lk3~lock
		make "t3 thing :d3
		$lk3~unlock	
The object lk1 lk2 and lk3 are the locker of the three detectors. The detector thread will then have been :
to th.Detector :lock :vtemp :vfail :engine 

	while (true) {
		if (thing :vfail) {
			$lock~lock
			make :vtemp (thing :engine) + (random 10 100)	
			$lock~unlock	
		} {
			$lock~lock
			make :vtemp thing :engine
			$lock~unlock	
		}
		Snooze 700000	
	}
end

6. Conlusion

Althought easier to create than in C++, a multi-threading application in Squirrel still requies careful thought in design to perform with success.