Cooperating Sequential Tasks

  1. Introduction
    1. Arduino
    2. Interrupts
  2. Simple Tasks
  3. Multiple Tasks
  4. Communicating Tasks
  5. Communicating Values
  6. Synchronising Tasks
  7. Buffered Communication
  8. Multiple Senders
  9. Conditional Tasks
  10. Transput
  11. Implementation
    1. Common Data
    2. Tasks
    3. task_builder
    4. task
    5. channel
    6. virtual_machine
    7. set
    8. queue
    9. clock
    10. standard
    11. Adjusting Limits


Data Structure

Every time new_task is called through the Tasks interface, a task_record, as defined below, is added to the tasks array (See Common Data).

const byte no_task      =  -1;
const byte max_tasks    =  10;
enum task_state {idle, active, waiting, waiting_with_arg, paused, sending, receiving, transputting};

struct task_record
  task_state      state;
  byte            id;
  int             first_act;
  int             next_act;
  int             start_time;
  int             pause_time;
  wait_condition  condition;
  wait_condition1 condition_with_arg;
  int             wait_data;
  data_source     source;
  data_sink       sink;
  byte            alt_output;
  byte            alt_input;
  int             task_data;
  bool            repeating;
  int             repeat_point;

The no_task constant defines the task identifier of the null task, i.e. when a task identifier is set to no_task, it does not refer to any task. The max_tasks constant defines the total number of tasks the system will support. This must be less than or equal to 16 because of the implementation used for task sets. Task identifiers will always be between zero and max_tasks minus one.

The individual fields of a task_record are described below.

task_state state;

As tasks are specified in a program, they are placed in the idle state and added to the back of the queue of potentially active tasks. When a task reaches the front of the queue, it will be placed in the active state and executed by the virtual machine until some action sets its state to other than active. Tasks waiting for a condition to become true will either be in the waiting state or the waiting_with_arg state. Tasks that have executed the pause action will enter the paused state, and tasks that are waiting for a communication will either be in the sending, receiving or transputting state. If a task executes the terminate action, it will be removed from the queue and placed in the idle state.

byte id;

Each task has an id (identifier) which is the index of its task_record entry in the tasks array. Many operations in the library refer to tasks via pointers to their task_records. Including the identifier in the task_record gives such operations access to it without having to search the tasks array to determine the task's index value.

int first_act;
int next_act;

The first_act field represents the position of a task's first action in the code array.

The next_act field is used by the virtual_machine as a virtual program counter to determine which of a task's actions to execute next. It is initialised to the value of first_act. When an action is executed, the next_act index will be advanced over the code for the action and any of its arguments, so that it will then refer to the following action. After the last action in a task's code has been executed, the virtual machine will reset next_act to have the same value as first_act.

int start_time;
int pause_time;

The start_time field is used to record the clock time when a task executes a pause action. The clock component is responsible for providing the time in milliseconds. When a task executes a pause action, it is removed from the queue of potentially active tasks, its state is set to paused, and its identifier is added to the set of waiting tasks.

The pause_time field is used to store the duration of a pause in milliseconds. Given the start_time for a pause and its duration, the clock component provides a time_up function which allows the virtual machine to determine when a task can be resumed at the end of a pause.

wait_condition  condition;
wait_condition1 condition_with_arg;
int             wait_data;

These fields are used to record the information needed to implement the wait action, with or without an argument. When a task executes a wait action, it is removed from the queue of potentially active tasks, its state is set to either waiting or waiting_with_arg, and its identifier is added to the set of waiting tasks. If the wait does not involve an argument, a pointer to a wait_condition function is stored in the condition field. If an argument is supplied, a pointer to a wait_condition1 function is stored in the condition_with_arg field, and the supplied argument value is stored in the wait_data field. The virtual_machine will used this information to periodically evaluate the wait condition to determine when the waiting task can resume execution.

data_source source;
data_sink   sink;
byte        alt_output;
byte        alt_input;

These fields are all used to implement the communication actions send_to, receive_from and transput. They are explained in more detail in the channel and virtual_machine sections.

int  task_data;
bool repeating;
int  repeat_point;

The task_data field is set by using the set_task_data function when a task is defined. The value of this field is returned to a task when it calls the get_task_data function from any of its action functions, wait conditions, or communication source and sink functions.

bool repeating;
int  repeat_point;

The repeating field is used to implement the repeat action. The virtual machine will set it to true if the condition function specified as an argument to repeat action returns true. The repeat_point field will be set to the value of the virtual program counter that refers to the repeat action itself. Following the execution of any action, the repeating field is checked and, if it is set to true, next_act is set to the repeat_point. Skipping the action following repeat is handled using the same mechanism used to implement when actions and does not require specific information to be stored in the task_record.


The following functions are provided by the task component for the benefit of the virtual machine.

void check_waiters();

The check_waiters function is used by the virtual machine to check to see if any paused tasks have reached the end of their pauses, and if any waiting tasks can be resumed because their wait conditions return true. This uses the waiting_set found in the common data and distinguishes waiting tasks from paused tasks by looking at the state field in their task_records.

void make_executable(byte id);
void make_executable(set* waiters);

These functions are used to restore tasks, which have been suspended, i.e. paused, waiting or communicating, to a potentially executable state. The task or tasks concerned will be placed at the back of the active_tasks queue for eventual execution. This operation can either be applied to a single task given its identifier, or to a set of tasks. The latter operation is used by the communication actions when several tasks can be resumed as the result of a single communication.

void delay_task(task_record* t);
void suspend(byte id, task_state new_state);

The delay_task function causes an active task to be moved to the back of the active_task queue. In effect, this implements a form of pre-emptive scheduling, e.g. when a task executes the last action in its list of defined actions, its virtual program counter next_act is moved back to the first action in its definition, in addition delay_task is used to send it to the back of the active_tasks queue. This ensures that tasks, which do not contain any actions that might cause them to be delayed, do not block execution of all other tasks.

The suspend function removes the indicated task from the active_task queue and sets its state to new_state, e.g. this is used in the implementation of the terminate action.