Tasker Tutorial


By Andrea Galimberti

Download the whole tutorial text and the example source code by clicking here!


In this tutorial program I'll describe how to launch two tasks that run asynchronously to the main program using the Tasker class (V1.02) belonging to the Amiga Foundation Classes package. The main program starts these two tasks and only waits for the close gadget of its window to be pressed: then it kills its child tasks before quitting. The code of the two tasks is contained in the drawsquare() and drawcircle() procedures.

I begin with the description of the main() procedure. The first instruction we meet, after some DEFs, is storea4(): this procedure stores the global data pointer that the E language keeps in the A4 register. The storea4() statement is needed if you want to use in the child task some global variables or if you want to call from the child task another procedure defined in your program (or in an external module; this doesn't apply to system calls, obviously). After you stored the A4 register you have to retrieve it once for any task started that needs to access global data: you do it by inserting a geta4() statement at the very beginning of your task code; you can see that this instruction is present in both our tasks (I'll describe them in more detail later).
The next step is opening a simple window that accepts only the IDCMP_CLOSEWINDOW message from Intuition (that is to say the window hears only about its close gadget being pressed): its pointer is stored in the mainw variable. If we cannot open this window we Raise() a "WIN" exception and exit from the program; otherwise we get the pointer to the window userport (up:=mainw.userport) and then we use the sigbit of this port to create the signal mask to be used in the Wait() statement (upsig:=Shl(1,up.sigbit)). This is the standard way to wait for a signal coming from a specified port: the upsig signal mask uniquely identifies the userport of this window (and the message that the close gadget has been pressed is expected to arrive through this port). I suppose you are already familiar with the Intuition mechanism (and some Exec basic notions), but I'll say more on it when we talk about the Wait() function. Note that to access the window structure pointed to by the mainw variable (to get the userport address) this variable must be DEFined as PTR TO window (a general LONG variable it's not enough); the same goes for the up variable DEFined as PTR TO mp (message port). The definition of window is contained in the 'intuition/intuition' module, and the one of mp in the 'exec/ports' module.

Then we build our two Tasker objects:


                  NEW dsquare.tasker('DrawSquare',FALSE)
The dsquare object is ready to accept data for a task that, when started, will be called 'DrawSquare'. The FALSE flag says that this task mustn't be killed when the dsquare object is ENDed. You have to supply a name for your task because Exec needs a name to identify its tasks; on the contrary the flag is optional: by default when you END the Tasker object the associated task will be deleted (if still present). The next line

                  dsquare.code({drawsquare})
says that the code of the DrawSquare task starts at the address of the drawsquare() procedure. The task stack defaults to 4000 bytes, and I don't need to modify this parameter.
The DrawCircle task is initialized the same way:

                  NEW dcircle.tasker('DrawCircle')
                  dcircle.code({drawcircle})
Note that the two tasks are not running yet.

Now the main() program needs a message port to communicate with the child tasks: we create such a port by calling the buildPort() procedure


                  mainport:=buildPort()
If something went wrong we Raise() the "PORT" exception and quit from the program; otherwise we build the signal mask for this port in the same way we did for the userport of the main window: mainport is DEFined as PTR TO mp to access its sigbit.

We are ready to start the two tasks:


                  dsquare.start()
                  dcircle.start()

Then we allocate the memory for a message structure and initialize it:


                  NEW mes
                  setupMsg(mes, SIZEOF mymsg, mainport)
The mes variable is DEFined as PTR TO mymsg; note the definition of the mymsg structure:

                  OBJECT mymsg
                    mnode:mn
                    cod:LONG
                    num:LONG
                  ENDOBJECT
Every custom message structure you define must begin with a mn (message node) structure followed by whatever you like (the mn structure is defined in the 'exec/ports' module). After we have allocated the necessary memory for our message (NEW mes) we have to initialize the mn part: this task is accomplished by the setupMsg() procedure. This procedure fills in the mn part of mes with the size of the whole message structure and the address of the port where the message will be replied (mainport).

In the following REPEAT cycle we wait for the user to press the mainw close gadget, then we send to the DrawSquare task the message to close itself and wait for the answer to this message; only when we get this answer we can safely quit. I'm going to describe in detail the contents of this cycle.
The line


                  ssig:=Wait(upsig OR msig)
waits for a signal coming from the userport (of mainw) or the message port mainport: the two ports are identified by their signal mask, and these masks are ORed because we are expecting a message from one port OR the other. The main program is put to sleep: when a signal arrives, the signal mask of the port that received it is stored in the ssig variable. So if we bitwise AND ssig with the original signal mask of one of our ports we can detect which port received the signal: if the statement

                  IF (ssig AND upsig)
results to TRUE then the signal arrives from the userport, otherwise it comes from the other port.

The first WHILE cycle gets all the intuimessages queued to the userport (the imsg variable is DEFined as a PTR TO intuimessage; the intuimessage definition is contained in the 'intuition/intuition' module):


                  WHILE (imsg:=GetMsg(up))<>NIL
If the message class is IDCMP_CLOSEWINDOW we send a message to the DrawSquare task, otherwise we simply reply to the message

                  ReplyMsg(imsg)
Note: you always have to reply to a message to signal that you no longer need the message data and so the sending task can recycle (or dispose) the memory allocated for that message.
Before sending our message we fill in its two fields:

                  mes.cod:=COD_MYMSG
                  mes.num:=-1
In the cod field we put a number identifing our message type, so the DrawSquare task can recognize it; in the num field we put the command to be sent to the child task: -1 means "kill yourself and quit". Then we send it:

                  dsquare.send(mes)
The send() method knows if dsquare has created a port and, if it is so, it sends the message mes to the address of this port; if it cannot send the message (perhaps because the port doesn't exist) it returns FALSE. A task, when started, hasn't got a port: you have to create it with the buildport() method before the task can receive any message. We will see how (and where) to do it later on when we'll describe the drawsquare() procedure.
Obviously you do not have to put in your message an identification code or something similar, but it's a good habit to tell the receiving task who is sending the message, even if you are sure you are the only one that sends messages to that particular task (in a multitasking environment you are never sure of anything; take this for granted!).
After it sent the message, the main program goes asleep again, waiting for another signal (and, in particular, waiting for the answer coming from the DrawSquare task).

The second WHILE cycle is invoked if the signal was received from the mainport: it gets all the messages queued to the mainport


                  WHILE (msg:=GetMsg(mainport))<>NIL
The msg variable is DEFined as PTR TO mymsg. Note: we do not use mes to get the messages from mainport because mes still points to a memory area that we are supposed to dispose before exiting the program; so writing to mes means loosing the address of the memory area. Besides, when we are sure our message has been replied, we can recycle the memory pointed to by mes to send another message of the same type (after having filled in its fields).
We recognize the message as an answer from DrawSquare if its cod field contains the number COD_REPLYMYMSG

                  IF msg.cod=COD_REPLYMYMSG
then we check if it is the answer to the -1 ("kill yourself") command:

                  IF msg.num=-1 THEN quit:=TRUE
if it is so we are satisfied and we quit.
If cod is different from COD_REPLYMYMSG it's not the message we were waiting for and we don't know what to do with it, so we simply reply and return to sleep. Obviuosly we do not reply if the message is an answer!

Now the DrawSquare task is killing itself, and the main task is going to dispose all the resources allocated and to kill the DrawCircle task that has no port (so we cannot tell it to quit).
We first close the mainport using the endPort() procedure


                  IF mainport THEN endPort(mainport)
then we close the DrawCircle task window (I'll explain later why we do it in the main task)

                  IF dcwin THEN CloseWindow(dcwin)
we close the main window

                  IF mainw THEN CloseWindow(mainw)
we dispose the dsquare object: remember that the DrawSquare task is not deleted by this instruction because of the FALSE flag we used when we created the object (we don't kill the DrawSquare task because it is already killing itself)

                  IF dsquare THEN END dsquare
we END the dcircle object (the DrawCircle task is killed by this instruction and the object is disposed)

                  IF dcircle THEN END dcircle
we dispose the memory allocated for the message

                  IF mes THEN END mes
and, if some exception has been raised (by our program or by the Tasker module) we write in detail what has happened

                  explain_exception()
The explain_exception() procedure is contained in the AFC module 'AFC/explain_exception'.

Now it's time to say something about the two subtasks: I begin with the drawsquare() procedure. The first instruction we meet is geta4(): we restore the global data pointer in the A4 register because we need to use the dsquare object. In fact the general rule is to DEFine a Tasker object as a global variable because it is used by the main() task and by the task it points to: the main() task usually needs that object to start the task and to send it some messages, while the child task needs it to build a message port and to get messages from its port.
Immediately after having retrieved the global data pointer we build a message port associated with the task:


                  dsquare.buildport()
Now buildport() is a method: it works just like the buildPort() procedure, but it doesn't return the port address because it stores such address in the dsquare object. Next we open a window and we store the address of its rastport to draw something in it:

                  rp:=win.rport
We build the signal mask associated to the message port created:

                  pp:=dsquare.port()
                  mysig:=Shl(1,pp.sigbit)
The port() method returns the address of the message port created with the buildport() method. Then we wait for a signal and, when the signal arrives, we check if there's some message queued at our port. The REPEAT and the following WHILE cycles work the same way as those contained in the main() task, so refer to that part of the tutorial for more informations. The difference is we cannot Wait() for a signal because in such a case the DrawSquare task will be put to sleep, and we need to do some processing while waiting for a message: so I used the SetSignal() call to check for a signal

                  sigs:=SetSignal(0,mysig)
Like Wait(), the SetSignal() function returns the signal mask received, but it doesn't stop the task; the drawback is that it doesn't clear the signal bit after having received it (you must clear such a bit to enable your port to receive further signals: the Wait() function does this automatically), so we have to do it by ourselves: the parameters (0,mysig) mean that the mysig signal will be cleared.
Note that we change the identification code of the message before replying:

                  msg.cod:=COD_REPLYMYMSG
and the DrawSquare task will quit if it receives the "kill message":

                  IF msg.num=-1 THEN quit:=TRUE

The remaining part of code draws randomly some filled rectangles in the window. Remember to close the window and the message port before quitting the task:


                  IF win THEN CloseWindow(win)
                  dsquare.endport()

The drawcircle() procedure is even easier: we retrieve the global data pointer (geta4()) because we need to use the dcwin global variable to store the address of the window we are going to open. But why don't I use a local variable as I did in the drawsquare() procedure? The DrawCircle task doesn't know when it's time to quit because I'm not going to open a message port for this task: so the window pointed to by the dcwin variable will be closed by the main() task before deleting the DrawCircle task. The drawcircle() procedure then enters an infinite loop and keeps on drawing circles in its window until the main() task stops it.

This is a relatively complex example of the multitasking capabilities of our Amiga and of the Tasker module. To conclude, the Tasker module makes it easier to manage different tasks at a time, but it's no foolproof: you always have to be careful when playing with tasks, because they are probably one of the most difficult parts of the Amiga Exec.