,---------------. | Contributions | `---------------´ ,------------------------------------------ | 2022-07-18 00:03:00 ruv wrote: | comment - Better API for multitasking | see: https://forth-standard.org/proposals/multi-tasking-proposal#contribution-247 `------------------------------------------ ## 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](https://gist.github.com/ruv/69b330269234b9b13d1563b2beeb120f) for MS-DOS many years ago, and I use [multi-threading in SP-Forth/4](http://spf.sourceforge.net/docs/intro.en.html#task) 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, or `bye` 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 termination - `wait-task-for ( u.timeout tid -- flag ior )` — wait termination for given timeout - `task-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 *tid*s 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 of `wait-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).