Index

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

Communicating Values Between Tasks



So far we have only used the send and receive actions to synchronise two tasks, i.e. one task signals another to tell it when to do something. This is the simplest case where no information is communicated between tasks. However, it is quite common to require one task to communicate data to another. We used the send_to and receive_from actions with a common channel parameter to connect them together. If we are going to send data from one task to another we also need to specify a function to provide the data and another to receive it, i.e. a source and a sink.

In a sending task we might declare a source function that always returns the value of a particular variable.

int x = 0;

int source()
{
  return x;
}


In a receiving task we might declare a function which assigns its single parameter to a particular variable.

int y = 0;

void sink(int x)
{
  y = x;
}


If the sender performs the action send_to(c, source) and the receiver performs the action receive_from(c, sink), where c denotes the same channel in both cases, the value of x defined in the sender will be assigned to the variable y in the receiver at the point both tasks are ready to communicate, i.e. y will be synchronised with x at that point in time. This is a safe way of sharing data between tasks.

Where the sender had access to both x and y it could use an action routine to assign x directly to y, but this would be dangerous because the receiver might be in the middle of using y and not be expecting it to change. Sharing data between tasks can cause all sorts of unexpected behaviour if not very carefully managed. Synchronised communication provides an effective way of doing this.

If a sender does not provide a source of data, but the receiver provides a sink, the behaviour will be the same as if the sender had sent a zero and the sink function will still be called in case it has side effects. If the sender provides a data source, but the receiver does not provides a sink, the data will be ignored, however the source function will still be called in case it has side effects.

The following definition is for a task which will accept characters from a serial input and send them, one at a time, via a channel to another task.

#include "Tasks.h"

void serial_in_setup(int baud_rate)
{
  init_serial(baud_rate);
}

bool have_character()
{
  return Serial.available() > 0;
}

int get_character()
{
  return Serial.read();
}

void serial_in(int character_stream, int baud_rate)
{
  new_task(serial_in_setup, baud_rate);
    wait(have_character);
    send_on(character_stream, get_character);
  end_task();
}


Instead of using the conventional method of initialising the Arduino Serial library directly, serial_in_setup uses init_serial which is an operation provided by the multiprocessing library for the purpose. This is because initialising the built in library more than once can cause problems, however init_serial will only have an effect the first time it is called. Thus, if one task needs to read from the built-in Serial library and others want to write to it they can all call init_serial without causing any problems as long as they agree on the baud rate to be used. In practice, the first call to init_serial will establish the baud rate, but given a set of concurrently executing tasks it might be difficult to guarantee which one will execute its setup operation first.

The first action the task will take is to wait for at least one character to become available. The appropriate wait condition is defined by the have_character function. Once a character is available a send_to action can be used to specify the channel to be used and the source function which will return the character value, get_character in this case. A task like this provides a useful way of encapsulating access to the Serial library so that it can be used safely by other tasks. Clearly having multiple tasks inputting characters from the same serial source would be a problem and should be avoided.

The next task receives characters from a channel and prints them out using the Serial library.

#include "Tasks.h"

void serial_out_setup(int baud_rate)
{
  init_serial(baud_rate);
}

void put_character(int character)
{
  Serial.print((char)character);
}

void serial_out(int character_stream, int baud_rate)
{
  new_task(serial_out_setup, baud_rate);
    receive_from(character_stream, put_character);
  end_task();
}


This task only needs one action. The call to the receive_from function specifies the put_character function is to be used to accept the received character from whatever channel the serial_out task is connected to. The put_character function simply prints the character using the Serial library. Note that the char cast is required because we want to print a character and not the code which has been used to represent it as an integer.

The following setup and loop functions show a simple example of the use of the two previous tasks.

#include "Tasks.h"

void setup()
{
  init_tasks();

  const int baud_rate = 9600;
  
  int character_stream = new_channel();
  serial_in (character_stream, baud_rate);
  serial_out(character_stream, baud_rate);
}

void loop()
{
  run_system();
}


We can describe this system graphically as follows.

images/5-1.png

If a serial terminal, e.g. the one built-in to the Arduino IDE, is connected to the serial pins on an Arduino running the program shown above, anything typed into it will be echoed straight back again.

Although it does not make any sense to have more than one task inputting from a serial connection, the same is not true for serial output. You might wonder what would happen if we used two serial_out tasks in the same program. Consider the following example.

#include "Tasks.h"

void setup()
{
  init_tasks();

  const int baud_rate = 9600;
  
  int character_stream = new_channel();
  serial_in (character_stream, baud_rate);
  serial_out(character_stream, baud_rate);
  serial_out(character_stream, baud_rate);
  serial_out(character_stream, baud_rate);
}

void loop()
{
  run_system();
}


A graphical representation of this system is shown below. Notice that there is still only one channel here. A system with one task connected to three different channels would be entirely different as the sending task could only send on one channel at a time.

images/5-2.png

We now have four tasks running, one inputting characters and three outputting, and they all use the same channel! Does this make any sense, or is it a recipe for disaster? In fact it is perfectly OK for a channel to have multiple receivers connected to it. The behaviour of send_to and receive_from in this case is a simple extension of the case where there is only one receiver. As we have already seem, both the sender and the receiver must synchronise in time before a communication can take place, i.e. if the sender sends before the receiver is ready it must wait and vice versa.

If there is more than one receiver, all of the tasks must synchronise in time, i.e. the sender must have sent and all the receivers must be ready to receive before the communication takes place. This is true if data is being communicated or not. If data is being communicated, as in the above example, all of the receivers will be sent the same data value provided by the sender. Therefore with a serial_in task sending to three serial_out tasks, all of the serial_out tasks will wait for a character to be sent and then output it to the serial connection. Thus, every character read from the serial connection will be echoed back three times. This is not particularly useful, but demonstrates quite well how multi-receiver communication works. It also demonstrates again that simple tasks can be reused to build a variety of more complex systems.

Next: Synchronising Multiple Tasks