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


Transput is the name given to communications that can either involve input or output. So far our simple tasks have either been able to send a signal and potentially wait for one or more receivers to accept it, or wait to receiver a signal. Because each task in purely sequential it cannot do two things at the same time and that includes sending and receiving. However, the multitasking library provides a further communication primitive called transput which enables a task to elect to send or receive a signal before continuing.

Here is a very simple task which uses a transput action. It is similar to the buffer task from the previous example, but behaves more like an updatable cache in that it is able to accept new data or send out the current data in any order.

#include "Tasks.h"

int cache_data = 'A';

void put_data(int data)
  cache_data = data;

int get_data()
  return cache_data;

void updateable_cache(byte input, byte output)
    transput(input, put_data, output, get_data);

As with the send_on and receive_from actions, transput can be specified with or without data being communicated. The four possibilities are shown below.

transput(input, output);               // Receive from input channel or send on output channel with no data.

transput(input, sink, output, source); // Receive data using sink function and send data from source function.

transput(input, output, source);       // Receive signal only, but send data from source function.

transput(input, sink, output);         // Receive data using sink function, but send signal without data.

The next, more detailed, example will describe how a single task can provide access to a multi-place buffer with values stored in an array. This is in contrast to the previous buffer example where individual buffer tasks only held a single data value and multi-place buffers required the use of one task for each element.

The following task keeps data in a buffer_data array with buffer_size elements. It is accessed via two index variables, buffer_in and buffer_out. Both of these index variables are initialised to zero to refer to the first element in the array. New data in entered into the array at the position indicated by the buffer_in variable, which is then advanced cyclically, i.e. if it gets to the end of the array, it will be moved back to the beginning again. Data is taken out of the array from the position indicated by the buffer_out variable, which is then also advanced cyclically.

Cyclic addition is implemented using the C++ modulus (%) operator. For example, if executed repeatedly, the following statement will cause the variable ā€œiā€ to take values 0, 1, 2, 0, 1, 2, 0, 1, 2... assuming it is initially zero.

i = (i + 1) % 3;

A separate buffer_entries variable is used to indicated how many data values are stored in the array. It is initialised to zero, incremented when data is added, and decremented when data is removed. This allows the status of the buffer to be maintained by the functions buffer_empty, buffer_full and buffer_intermediate. The last of these returns true when the buffer is neither empty nor full.

It is important that data is not added to the buffer when it is full, or existing data would be overwritten, and that data is not read from the buffer when it is empty. When the buffer is in the intermediate state, it can be either read or written. The conditional when action is used in the body of the task to achieve exactly this. When buffer_empty returns true, the buffer can only receive new data. When buffer_full returns true, the buffer must send out existing data to make room for more. When buffer_intermediate returns true the buffer can either receive new data or send out an existing value using the transput action. The task definition almost writes itself.

#include "Tasks.h"

const int buffer_size = 8;

int  buffer_data[buffer_size];
byte buffer_in      = 0;
byte buffer_out     = 0;
byte buffer_entries = 0;

void put_buffer(int data)
  buffer_data[buffer_in] = data;
  buffer_in              = (buffer_in + 1) % buffer_size;
  buffer_entries         = buffer_entries + 1;

int get_buffer()
  int data       = buffer_data[buffer_out];
  buffer_out     = (buffer_out + 1) % buffer_size;
  buffer_entries = buffer_entries - 1;
  return data;

bool buffer_empty       () { return buffer_entries == 0;                                }

bool buffer_full        () { return buffer_entries == buffer_size;                      }

bool buffer_intermediate() { return 0 < buffer_entries && buffer_entries < buffer_size; }

void buffer(byte input, byte output)
    when(buffer_empty       ); receive_from(input, put_buffer);
    when(buffer_full        ); send_on(output, get_buffer);
    when(buffer_intermediate); transput(input, put_buffer, output, get_buffer);