Digest #190 2022-07-18
Contributions
Task object problem
The proposed interface (partially) is:
TASK ( "name” -- )
, name( -- a-addr.task )
/TASK ( -- u )
— This word allows arrays of tasks to be created without having to name each one.CONSTRUCT ( a-addr.task -- )
— Instantiate the task at a-addr.task.START-TASK ( xt a-addr.task -- )
The task shall not be restarted [by means of
START-TASK
] until it has terminated.
It's a significant condition that I oversight before.
How it can be known in the parent thread whether a child thread has terminated or not? How to correctly free the memory allocated for a "task"?
Actually, it's a problem — since the user should manage memory for "tasks" on a very low level, and there is no enough means for that. But even having such means, this resource management should be hidden under the hood.
Requirements for multitasking
I implemented a variant of cooperative round-robin multitasking in Forth for MS-DOS many years ago, and I use multi-threading in SP-Forth/4 on Windows (and Linux) a lot. In these both implementations a data object that is required to start a new thread was superfluous (and it could be eliminated).
In practice, I need a convenient way to:
- launch an xt execution in a new separate thread (i.e., this xt is executed asynchronously, in parallel, using separate stacks),
- pass at least one argument to the new thread for this xt:
xt ( x -- )
, - premature terminate execution of the own thread (it's like
exit
for a word, orbye
for a process), - forcefully terminate execution of another thread,
- wait for a thread termination,
- get a throw code of a terminated thread.
I'm not sure concerning the following:
- suspend execution of own or another thread (if it was not terminated),
- resume execution of another thread (if it was suspended, otherwise do nothing),
I don't include synchronization primitives in the list since they are a slightly different topic.
Another API variant
According to requirements, I can suggest the following API to manage tasks (threads):
launch-task ( x xt -- tid ior )
close-task-id ( tid -- ior )
clone-task-id ( tid1 -- tid2 ior )
— a method to obtain another independent identifier.task-id-own ( -- tid.own )
terminate ( -- )
kill-task ( tid -- ior )
wait-task ( tid -- ior )
— wait task terminationwait-task-for ( u.timeout tid -- flag ior )
— wait termination for given timeouttask-status ( tid -- n.status-code ior )
Optionally:
launch-task-suspended ( x xt -- tid ior )
suspend-task ( tid -- ior )
resume-task ( tid -- ior )
The method to suspend a task is not quite useful, since it cannot be used for synchronization.
Any tid is valid until it's closed (freed). The own tid is closed automatically when the thread terminates, so it should not be passed to another tread. When all tids are closed, the resources are freed automatically. NB: these identifier are not unique, since many identifiers may identify the same task/thread. Maybe the term "handle" is suited better here than "identifier".
Shared resources considerations
The API should specify how to work with dynamic memory: may a memory block allocated in one thread be freed in another thread, or not. For example, in SP-Forth/4 memory is allocated/freed in the per thread heaps, and all the allocated memory blocks in the thread are freed when the thread is terminated; and the global heap is also available.
Also, the API should specify does here
return different values in different threads, or the same.
If here
can be local for threads, may these threads perform evaluate
and/or included
independently in parallel, or not. If yes, should the compilation word list be different in different threads, or not.
High level inter-thread communication
A very convenient way for communications are channels (that is a message queue under the hood). So, each running task has an associated channel for inbound messages. Usually a message is cell-sized number. Maybe be strings should be also considered.
A sketch of methods:
send-message ( c-addr u tid -- ior )
— sends a copy of given string.wait-message ( -- c-addr u )
— the returned string is valid until the next call ofwait-message-*
.wait-message-for ( u.timeout -- c-addr u flag )
The meaning of the content of string ( c-addr u ) is defined by the program (it may be binary data as well as textual).