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.
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.
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.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).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.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.
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.