In this section you will learn how you can write multitasking programs using parallel programming,
available “out of the box” on ugBASIC. If you want to deepen the theory behind it,
I recommend reading this article.
Introduction
Define
Run
Synchronization
Variables
Wait
In computer science, multitasking means the support in the execution of multiple
flows of execution at the same time (in a "parallel" way). In systems with severe
memory constraints such as computers based on 8 bit processors, this cannot be obtained
using traditional techniques, because it takes too much memory since each parallel process
requires its own stack which, in turn, occupies large parts of the available memory.
For this reason a mechanism based on protothreads
has been implemented. This mechanism makes it possible to define procedures that will be executed
for appropriate “quanta” of time, without however the execution being interrupted unexpectedly.
In ugBASIC the PROCEDURE
s are stack less. This means that there is no stack
for local variables, but they are mapped (trasparently) into the memory. This
means that standard PROCEDURE
s are ready to be promoted for a parallel
approach. And in fact converting a sequential procedure into a parallel one is
very simple: just add the PARALLEL
keyword where you use the PROCEDURE
keyword.
For example, this procedure will print forever the string “example” on the screen:
PROCEDURE example1
DO
PRINT "example"
LOOP
END PROC
So you can call it using the standard syntax:
example1[]
If you want to convert this routine in a procedure that can be run in a multitasking fashion,
you have to use the PARALLEL
keyword, like that:
PARALLEL PROCEDURE example1
DO
PRINT "example"
LOOP
END PROC
When this keyword is added, the procedure can no longer be called directly from
the program but the keyword SPAWN
must be used. So:
SPAWN example
Now let's try to write down the following example program, and run it.
PARALLEL PROCEDURE example1
DO
PRINT "example"
LOOP
END PROC
SPAWN example
If you compile the source code with ugBASIC v1.14.1 or less, you can notice that,
on the screen, you will see nothing. Why? Because the SPAWN
command
will prepare only the system to run the procedure in a parallel fashion. You need
to decide when this will occour in your main program.
The command to use to invoke a “multitasking slice” is RUN PARALLEL
.
Whenever that command is invoked, all the procedures registered to run in parallel
mode are executed in the same “slice” of time. In order to make “permanent” execution
of the various programs in parallel, you need to place the call inside a DO...LOOP
.
For example, this is a very simple example. We will run two separates tasks: the first will
print the “example” string, while the second will print the “example2”. Since the two tasks
are running at the very same time on the processor, the two strings are printed alternatively.
PARALLEL PROCEDURE example1
DO
PRINT "example"
LOOP
END PROC
PARALLEL PROCEDURE example2
DO
PRINT "example2"
LOOP
END PROC
SPAWN example1
SPAWN example2
DO
RUN PARALLEL
LOOP
If you use the version 1.14.2 or better and you do not use RUN PARALLEL
,
the last three lines will be not needed, since they will be generated automatically
at the very end of the program.
Now let's introduce a way to synchronize the execution of two tasks.
In particular, this is the way to invoke a task inside another task,
and how to wait the end of execution of the inner task before continue.
PARALLEL PROCEDURE example1
FOR x = 1 TO 10
PRINT "example ";x
NEXT
END PROC
PARALLEL PROCEDURE example2
WAIT PARALLEL SPAWN example1
PRINT "example2"
END PROC
SPAWN example2
DO
RUN PARALLEL
LOOP
As you can easily verify by running this program, the second procedure will
execute only when the first has finished. In order to wait the execution, we
will use the WAIT PARALLEL
instruction. This command will take
the identification of the current task (“task id”), that is the valure returned
by the SPAWN
command.
As we said earlier, procedures do not have an independent stack.
This means that it is not possible to use local variables, at
least not directly. More precisely, each variable is shared among
all tasks (of the same type).
PARALLEL PROCEDURE example
DO
x = x + 1
PRINT x;" ";
LOOP
END PROC
In this example, the x
variable is shared among all invoked
tasks. So, if you invoke 5 tasks, the value of x
will be
increased monotonously as well as if you invoke just one task.
To overcome this problem you can use just a global array (x(...)
),
the size of which will be equal to the number of tasks concurrently in execution.
Each element of the x
array will store the variable value for that
specific task. To find out what is the number of the task being executed is,
you can use the keyword TASK
(or THREAD
).
Let's look at this example:
DIM x(3)
GLOBAL x
PARALLEL PROCEDURE example
DO
x(TASK) = x(TASK) + 1
PRINT x(TASK);" ";
LOOP
END PROC
SPAWN example
SPAWN example
SPAWN example
When running the example, you will see that the value of x
will be
incremented, separately, for each task.
Finally, you can use a shorter form by enclosing the variable name inside square brackets:
' [a] -> a(TASK)
PARALLEL PROCEDURE example
DO
[x] = [x] + 1
PRINT [x];" ";
LOOP
END PROC
So far we have seen how it is sufficient to define a procedure with the
PARALLEL
keyword to see it execute in parallel. However,
this is possible because ugBASIC implicitly generates the commands
needed to coordinate the various tasks. So, you can force the early
release of the time slice by using the YIELD
command.
For the same reason, it is not possible to busy wait for a certain
condition to be true or false, because this would prevent ugBASIC
from generating the appropriate escape routes.
For this purpose, two commands have been introduced:
WAIT UNTIL condition
WAIT WHILE condition
If you have found a problem, if you think there is a bug or, more
simply, you would like something to be improved, write a topic on the official forum, or open an issue on GitHub.
Thank you!