-
Notifications
You must be signed in to change notification settings - Fork 1
Task Concurrency
The task is the fundamental unit of concurrency in ParaVM. It can be thought of as an extremely lightweight thread; while it has its own execution stack, memory heap, and so on, those all grow and shrink as required such that only the bare minimum of system resources are used.
Tasks are scheduled differently depending on whether ParaVM is using the emulator or the JIT compiler.
The emulator uses regular Linux threads for scheduling tasks. If a system has e.g. 4 cores, the emulator will start 4 scheduler threads which will run through the various tasks in the system and execute a certain amount of instructions per task before switching to the next task.
Given N
scheduler threads, M
tasks (in the entire VM), and a priority value P
, a task will be allowed to execute (M / N) * P
instructions before its current scheduler thread switches to the next task. P
is either 1
, 2
, or 3
depending on whether the task has priority 'low'
, 'normal'
, or 'high'
, respectively.
Each scheduler thread has a queue of normal-priority tasks to run that the thread pops an item from once per scheduler loop iteration. Once a task has finished executing in a scheduler iteration, it's pushed back into the queue and will be executed again once the scheduler reaches it.
Low-priority tasks are managed in a separate queue, which the scheduler pops an item from every N
iterations (instead of popping from the normal task queue). High-priority tasks are also in a separate queue, and one high priority task is always executed per scheduler iteration (in addition to a task with low or normal priority).
The only way that tasks can communicate is through sending and receiving messages. There are no global variables.
When a message (which can be any data type) is sent to a task, a deep copy of the data is made, which is then sent to the target task's message box. If the task is no longer reachable (e.g. it has exited), the message is discarded.
A task can receive a message (e.g. with the plain receive
function) and then proceed to process it. This immediately removes it from the message box. If no message is present in the message box when a task calls receive
(or some variation thereof), it will yield control to the scheduler until a message arrives, or a timeout is reached.
Functions for dealing with messages (such as send
and receive
) are in the task
kernel module.
Some task A
can link itself to another task B
such that if B
goes down for some reason, A
will be notified. This is useful for a number of things:
- Shutting down an entire tree of tasks sanely.
- Handling errors in other tasks without the entire system failing.
- Restarting tasks when they go down due to an error.
When a task goes down, these are the messages it can send to tasks that have linked to it:
-
{'exit', t, 'normal'}
: The task exited normally. -
{'exit', t, 'kill', k}
: The task was killed by taskk
(address). -
{'exit', t, 'error', e}
: The task encountered an errore
(any data type).
t
is the address of the task that went down.
Every task has a local dictionary that maps arbitrary keys to arbitrary values. This dictionary is referred to as TLS (task-local storage). It's useful to maintain 'global' state (that is, state not stored in registers directly or indirectly) in tasks - for example, a random number generator might use it to store the current generator state.
It's important to note that keys and values in the TLS dictionary are kept alive for as long as they are present. This can result in excessive memory usage if care is not taken to remove keys and/or values that are no longer used.
Functions for interacting with the TLS dictionary are in the task
kernel module.