Multitasking in BlitzMax

BlitzMax Forums/BlitzMax Tutorials/Multitasking in BlitzMax

skidracer(Posted 2007) [#1]
The following is an attempt at a TaskManager that lets you run services "asynchronously" in BlitzMax.

Tasks may be serviced whenever, not just when you call WaitEvent, but when calling any BlitzMax function that causes a PollSystem including KeyDown, WaitEvent and many others.

There is protection built into the TaskManager that will stop services being reentrant so your callback will never be called while its running it's current refresh cycle (see _busy field).

Basically the following creates a multitasking environment where any services (functions) you need to call on a regular basis can be easily added to a tasklist.

Note how the tasks keep printing even when you are dragging the Max2D window, the trick is to never call any Max2D drawing from such tasks as I think this will create a deadlock in Windows. MaxGUI offers more flexibility for graphics rendering where the GADGETPAINT event can be used to synchronize Max2D drawing with the operating systems own expectations / requirements.

Similar to a multithreading environment you need to be careful to cooperate with the main program and any other tasks.

Also, as timers are a limited resource on some systems (16 per Windows app etc.), a single 100hz timer is used by the TaskManager to service all threads.

' blitzmax multitasking example

Strict

Global TaskManager:TTaskManager=New TTaskManager

Function task1()
	Print "task1"
End Function

Function task2()
	Print "task2"
End Function

TaskManager.StartTask 10,task1
TaskManager.StartTask 20,task2

Graphics 640,480

While WaitEvent()
	DebugLog CurrentEvent.toString()
Wend

End

Type TTask
	Field _hertz#
	Field _count
	Field _millis
	Field _ticks
	Field _callback()
	Field _busy

	Method Start(hz#,callback())
		_count=0
		_hertz=hz		
		_millis=MilliSecs()
		_callback=callback
		TaskManager._tasklist.AddFirst Self
	End Method

	Method Stop()
		_hertz=0
		TaskManager._tasklist.Remove Self
	End Method

	Method Update(ms)
		Local	count
		If _busy Or _hertz=0 Return	'ignore if stopped or already in callback 
		_busy=True
		count=((ms-_millis)*_hertz)/1000
		If count>_count
			_ticks:+1
			_count=count
			_callback()
		EndIf
		_busy=False
	End Method

End Type

Type TTaskManager
	Global _tasklist:TList 
	Global _timer:TTimer
	Global _event:TEvent
	
	Method New()
		_tasklist=New TList
		_event=CreateEvent( EVENT_TIMERTICK,Null,0 )
		AddHook EmitEventHook,hook
		_timer=CreateTimer(100,_event)
	End Method

	Function hook:Object( id,data:Object,context:Object )
		Local ms
		Local t:TTask
		If data<>_event Return data
		ms=MilliSecs()
		For t=EachIn _tasklist
			t.Update(ms)
		Next
	End Function

	Method StartTask:TTask( hertz#,callback() )
		Local t:TTask
		t=New TTask
		t.Start hertz,callback
		Return t
	End Method

End Type


Possibly all uses of the word Task should be replaced with Service as it is currently required that each task return from it's callback for the system to continue.

Will add more details after answering initial questions....

yes, you sir at the back -


ImaginaryHuman(Posted 2007) [#2]
Looks nice. It runs fine on my system here. I don't entirely understand all of it but I presume, to simplify, it's basically a way of calling given functions at a given time interval using a system timer, as if each function is a `thread` of sorts, as callback functions. So you can do whatever you want from that function and then cooperatively return back to the system. Can I presume this means that until you return from the function, the system is `locked in`? Is the o/s locked-in or just the Blitz application? What happens when the timer says its time for another function to be called, but the one you're in hasn't returned yet because it's doing processing? Does it just skip the call or put it in a queue or something?

It's a nice little system. What I had been doing is check the Millisecs() timer and manually switching functions. So does your system not waste any cpu time when it's waiting for it to be time to trigger the function? ie you have to PollEvent in some way, to give control over to the o/s, to know if there's an event waiting there, otherwise you come back and sit there polling?


H&K(Posted 2007) [#3]
I use this
http://www.blitzmax.com/codearcs/codearcs.php?code=1721
I just say at at at what milli delay to call each function, and it sort of mutiltasks

(Ok what a really do is have the one function saying what task gets the time, but its the same principle)

@Skid, dont forget we have a limit of 20timers ;)


skidracer(Posted 2007) [#4]
H&K, the task manager allows limitless tasks serviced from the single timer.

AD, I've had a bit of a look and it turns out PollSystem does not allow reentrant usage so 1 task can't run above another,

however, a quick squizz at brl.system, and the solution seems to be:

Function task2()
	For Local i=1 To 50
		Delay(10)	'simulate big jobs
		Print "task2"
		Driver.poll
	Next	
	Print "DONE!"	
End Function


If you modify the original code, you will see task 1 being serviced while task2 is chomping through it's loop. However there is no way (short of threading) execution can return to task2 until task1 ends, but it does allow for some useful solutions I think.


H&K(Posted 2007) [#5]
H&K, the task manager allows limitless tasks serviced from the single timer
That will teach me to read the whole listing ;)
So its more like my solution than I thought ;)

One of either Flame or Dream said that if Bmax was redone with the Newer MiniGW then it would be mutli thread safe. Do you know if this is true?


morszeck(Posted 2007) [#6]
this is dangerous. the function must run 1000%! otherwise there is a trailer. but it is an alternative.


H&K(Posted 2007) [#7]
OK, working on the assumtion that TTaskManager is a singleton.

Remove TaskManager:TTaskManager=New TTaskManager

Add this as a Global of TTaskManager
Global TaskManager:TaskManager=New TaskManager
Change TTaskManager to TaskManager and Change StartTask() into a Function

' blitzmax multitasking example

Strict

TaskManager.StartTask 10,task1
TaskManager.StartTask 20,task2

Graphics 640,480

While WaitEvent()		

	Select EventID()			
	
		Case EVENT_KEYDOWN
			
			If EventData() = KEY_SPACE
				PostEvent( CreateEvent:TEvent (EVENT_APPTERMINATE) )
			EndIf
	
		Case EVENT_APPTERMINATE
		
			End

	End Select

	DebugLog CurrentEvent.ToString()
Wend

End


'=======================================================================


Function task1()
	Print "task1"
End Function

Function task2()
	Print "task2"
End Function


'=======================================================================


Type TTask
	Field _hertz#
	Field _count
	Field _millis
	Field _ticks
	Field _callback()
	Field _busy

	Method Start(hz#,callback())
		_count=0
		_hertz=hz		
		_millis=MilliSecs()
		_callback=callback
		TaskManager._tasklist.AddFirst Self
	End Method

	Method Stop()
		_hertz=0
		TaskManager._tasklist.Remove Self
	End Method

	Method Update(ms)
		Local	Count
		If _busy or _hertz=0 Return	'ignore if stopped or already in callback 
		_busy=True
		Count=((ms-_millis)*_hertz)/1000
		If Count>_count
			_ticks:+1
			_count=Count
			_callback()
		EndIf
		_busy=False
	End Method

End Type

Type TaskManager

	Global TaskManager:TaskManager=New TaskManager
	Global _tasklist:TList 
	Global _timer:TTimer
	Global _event:TEvent
	
	Method New()
		_tasklist=New TList
		_event=CreateEvent( EVENT_TIMERTICK,Null,0 )
		AddHook EmitEventHook,hook
		_timer=CreateTimer(100,_event)
	End Method

	Function hook:Object( id,data:Object,context:Object )
		Local ms
		Local t:TTask
		If data<>_event Return data
		ms=MilliSecs()
		For t=EachIn _tasklist
			t.Update(ms)
		Next
	End Function

	Function StartTask:TTask( hertz#,callback() )
		Local t:TTask
		t=New TTask
		t.Start hertz,callback
		Return t
	End Function

End Type
This way, the initialization of TaskManager is automatic

(Note: Because the original wasnt a singleton, was the reason I felt that Skid should limet the number of TaskManagers to less than 20, however having thought about it, limiting it to one seems a better Idea)

Oh and press space or click on the X to stop the program, (Really Skid ;)


ImaginaryHuman(Posted 2007) [#8]
This is cool. But isn't it wasting quite a lot of time waiting for events when it could be processing?

And isn't there some delay between the timer ticking and the event being detected and used, ie it's inaccurate?

Also, does this only run one function at a time or can a shorter function pre-empt a longer one? And if so, isn't there a lot of time being wasted checking if the timeslice is expired or whatever? ie isn't it checking all tasks upon every timer tick, rather than just pre-empting to the highest priority task?


H&K(Posted 2007) [#9]
1) Yep, if the processing is shorter than the time allocated for it, the program sits in the event loop

2) Yes ish. If everything is running under spec, then the tick is "accuarate"(ish:- like dont launch anti missile systems from it), however if the eventloop is too long (so the system cannot poll the ticks), or
3) As the ticks arnt pre-emptive, the just stackup oneach other, thus lossing accuracy. Generaly you have to make sure that everything is happening on a tick, so that the eventloop is simply posting/processing window/system events and not doing any of the "Program", and secondly you have to ensure that all subprocces you have Self-empt (just made that up), after whatever is your time slice for it.


ImaginaryHuman(Posted 2007) [#10]
[deleted]


errno!(Posted 2007) [#11]
A Linear multi-task executor? PARADOX :)


ImaginaryHuman(Posted 2007) [#12]
It's not really able to pre-empt because you have to choose a time to go and look for events, to trigger the hook. Ideally when the timer ticks it would be a real hook, ie hooked into the o/s, so that the o/s itself calls the hook function immediately, pre-empting whatever else your app is doing. But it can't do that, from what I can tell. All it can do is wait for you to give control to the o/s so that it can transfer events to your app, so that Blitz can trigger the hook, which means having to wait and detect the presence of a timer tick. I don't see that there is really any way in Blitz to truly pre-empt. We don't have interrupts. We don't have the ability to make something happen when a timer ticks all by itself. We don't have a timer that can count down from a number and then trigger something by itself. We don't have threads. Processes can't really talk to each other. Hooks aren't really hooked into the o/s system functionality. The app is kind of isolated in its own world. So this is about as good as it gets - cooperatively multiasking with the o/s in order to get the events so that you can call your own hook. And like Skid said you can't really pre-empt because a function needs to end and return before the next one starts.

I've come up with a similar solution using a timer to trigger a scheduling event which pre-empts a current *script*, as part of a script interpretation engine, but then this is only possible by a) occasionally polling for events to trigger the timer tick event, and b) abstracting program execution away from real direct programs and into a script layer - once removed. It acts as though it is pre-emptive but in the script interpreter there is still that requirement to poll the system manually to cause a pre-emption, which in part is cooperative, not pre-emptive. Having some kind of interrupt-driven function would be great.