ERC CISST Software
Quick start for cisstMultiTask

Anton Deguet

Date: 2009/01/10 05:02:51


Contents

listingrgb0.9,0.9,0.95 language=C++, commentstyle=red, keywordstyle=blue, fancyvrb=true, numbers=left, numberstyle=, stepnumber=2, numberfirstline=true, numbersep=5pt, tabsize=4, frame=lines, backgroundcolor=listing

1 Introduction

These examples provide a quick introduction to the features of cisstMultiTask. The code is part of the CVS module cisst/examples/multiTaskTutorial. To compile your own code, remember to include cisstMultiTask.h. Per convention, all the symbols starting with mts are defined in cisstMultiTask. Symbols prefixed with cmn, osa and vct come from cisstCommon, cisstOSAbstraction and cisstVector, respectively.

To compile the examples provided in this document, you will need the cisst package as well as the FLTK libraries and its GUI builder (fluid) which can be download from www.fltk.org.

2 Main concepts

The cisstMultiTask library is intended to help users develop multi-threaded applications and provide a flexible interface between tasks. The base components of the cisstMultiTask library are:

2.1 Tasks and devices

A cisstMultiTask task corresponds to a thread with a periodic user defined function. For most applications, the programmer will have to write classes derived from this class, mtsTaskPeriodic, which will handle all the timing issues as well as the means to establish the communication with other tasks in a thread safe manner. Communication with other tasks is performed using the task interface class which will be introduced shortly.

The class mtsTask is a base class of mtsTaskPeriodic and has the same features except the periodic thread. This class assumes that an external library provides a mechanism to call a main Run method periodically. This is useful when a device comes with a driver or interrupt more precise than a software based periodic call.

For some specific cases, there might not be any need for a periodic computation (either because it is performed by an external device or because it is performed by another library). Even so, it can be very useful to interface to these devices as if they were mtsTaskPeriodics. The class mtsDevice, used as a base class, allows to create a wrapper which can be interfaced as a mtsTaskPeriodic.

2.2 Interfaces

To provide a flexible interface, the cisstMultiTask library uses the ``command pattern''. In this design, an object interface is not defined by its public methods but rather by a list of pointers on methods. The list of methods pointers (commands) can be defined and queried at run-time which provides much more flexibility than compile-time binding.

During initialization, a given task has to populate its interface object with pointers to existing methods. This task, the resource task, can then be used by another task and by convention, the interface containing the commands is called the provided interface. The user task will have to query the commands by name and group them in its required interface object.

Populating and querying the provided interfaces is performed using the mtsDevice, mtsTask and mtsTaskPeriodic functionalities. Internally these maintain a list of mtsDeviceInterface and mtsTaskInterface, respectively.

2.3 Commands

Each provided interface contains a list of commands. As internally the commands are method pointers, a limited number of signatures is supported. The simplest command does not take any parameters and is called mtsCommandVoid. To send a data object, one can use a write command (mtsCommandWrite) and to receive a data object one can use a read command (mtsCommandRead). Finally, cisstMultiTask provides a qualified read command (mtsCommandQualifiedRead) which allows to send one data object and retrieve one.

Internally, cisstMultiTask will use buffers to ensure thread-safety. For a write or void command sent to a task, a mailbox will be used. For a read command, one should use a special buffer also built in mtsTask, i.e. the state table. To summarize, all void and write commands are queued by default, i.e. the execution of the actual method or function will occur in the thread space of the task who dequeues the command. All read and qualified read commands are not queued, i.e. they are executed in the thread space of the caller. Programmers will have to be carefull when creating read commands not based on the state table (see next section).

Since the signatures of the methods used for the commands must all match, all the parameters must be derived from a base class. This base type, mtsGenericObject, is defined in cisstMultiTask. Convenience types are also defined for native types such as mtsDouble, mtsInt... These classes are mere proxies to the actual data which can be accessed using the Data data member. For non native types, cisstMultiTask provides mtsVector (derived from vctDynamicVector), mtsMatrix (derived from vctDynamicMatrix) and many transformation types (e.g. mtsDoubleQuatRot3, mtsDoubleMatFrm3, ...). The later types use multiple inheritance so they can be used as their cisstVector counterparts.

2.4 State data and table

Each task owns a state table (mtsStateTable) which can be used to store the state of the task (the data member is mtsTask::StateTable). The table is a matrix indexed by time. At each iteration, one or more data objects used to define the state (mtsStateData) are saved in the table. At any given time, the task can write in the last row while anyone can safely read the previous states (including from other threads/tasks).

The state table length is fixed to avoid dynamic re-allocation. Its size is defined by a mtsTask constructor parameter. The table will not overflow because it is implemented as a circular buffer.

The class mtsStateData provides easy ways to create commands to access the state table. These can be used in the task interface.

2.5 Connecting tasks interfaces

Once all the tasks are defined, it is necessary to connect them. Each task or device can have multiple provided interfaces. Reciprocally, a task may have multiple required interfaces, i.e. a task ``user'' can connect to multiple provided interfaces provided by one or more ``resource'' tasks. For each interface provided by a resource, a user task must define a required interface. To manage the devices, tasks and their connections, use the cisstMultiTask class mtsTaskManager.

3 Two tasks communication

In this example, we will create two tasks: a sine wave generator and a data display. The communication works both ways. The display task will be able to read the last value created by the sine wave generator. The display task will also be able to change the sine wave amplitude by sending/writing the new amplitude. The sine wave generator interface is designed so that it could be replaced by any other signal generator. Hence the commands are called ``GetData'' and ``SetAmplitude''.

3.1 Sine wave generator task

As mentioned earlier, the sine wave generator is derived from mtsTaskPeriodic:

[firstline=11,lastline=33, basicstyle=, caption=sineTask.h] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example1/sineTask.h

Besides the declaration of the constructor and the methods Configure, Startup, Run and Cleanup, the class declaration must define all the state data we intend to use. To declare the state data objects, we used the templated class mtsStateData. The template parameter is the actual type of the data. As we plan to pass these objects as parameters for the interface commands, we used mtsDouble which is derived from mtsGenericObject.

The two most significant methods are the constructor and the Run method.

[firstline=11,lastline=26, basicstyle=, caption=sineTask.cpp: constructor] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example1/sineTask.cpp

In the constructor, we added the state data ``SineData'' to the state table and then created the provided interface ``MainInterface'' (for lack of better name). We then used the state data objects which provide methods that can be bound to commands. At run-time, the instantiated task will have a ``MainInterface'' with the read command ``GetData'' and the write command ``SetAmplitude''. Both of these commands will operate directly on the state table in a thread safe manner.

[firstline=32,lastline=40, basicstyle=, caption=sineTask.cpp: Run method] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example1/sineTask.cpp

The second significant method is Run. We first use the state table indexing to retrieve a time tick. This time tick is used to represent time and compute the sine wave data. Before this computation, the call to ProcessQueuedCommands ensures that all posted commands are processed (in this case, the only possible queued command is the write command ``SetAmplitude'').

3.2 Display task

The display task for this example is implemented using FLTK. The code for the GUI itself has been generated using the FLTK GUI builder, fluid. The user interface event loop has been replaced by our own display task periodic method Run. As for the sine wave generator task, the display task is derived from mtsTaskPeriodic.

[firstline=11,lastline=37, basicstyle=, caption=displayTask.h] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example1/displayTask.h

The class declaration is very similar to the sine wave generator except that we don't need any state data and we declared two mtsFunctions. These objects are helpers to provide a more familiar syntax than commands. They nevertheless rely on the actual commands as provided by the resource task. The implementation is:

[firstline=11,lastline=20, basicstyle=, caption=displayTask.cpp: constructor] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example1/displayTask.cpp

In the constructor, we did not define any provided interface as this task is not intended to be used as a resource. But we had to add a required interface to be able to connect to a sine wave generator.

[firstline=37,lastline=41, basicstyle=, caption=displayTask.cpp: Startup method] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example1/displayTask.cpp

The Startup method is called just before the task begins periodic execution. In this case, it initializes the AmplitudeData and displays the GUI.

[firstline=43,lastline=68, basicstyle=, caption=displayTask.cpp: Run method] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example1/displayTask.cpp

The Run method gets the data from the signal source and displays it on the UI. It then checks if the amplitude has been modified by the user and if so sends a command to the signal source. The important thing to note is that all actions on other tasks can be performed using the mtsFunction objects which have the ``look and feel'' of regular C/C++ functions.

3.3 Main program

The goal of the main function is to declare the tasks, connect them and then start them:

[firstline=29,lastline=62, basicstyle=, caption=main.cpp: first example] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example1/main.cpp

The first part of the main function (not shown) is dedicated to the log control, i.e. add std::cout as an output for the log and add a log per thread using osaThreadedLogFile. Log files named ``example1-0,1,2.txt'' will be created, each one containing the log for a given thread (one for the main, one for each task).

The second part is the creation of the tasks and their insertion in the task manager. The important call is:

taskManager->Connect("DISP", "DataGenerator", "SIN", "MainInterface");
This tells the task manager to connect the required interface ``DataGenerator'' of the task ``DISP'' to the provided interface ``MainInterface'' of the task ``SIN''.

The method ToStreamDot generates a ``dot'' file. ``dot'' is a nice program that generates graphs from a text description (see fig 1). For more information, visit www.graphviz.org.

Figure 1: Example 1: tasks graph
\includegraphics[scale=0.5]{example1}

4 Adding events and a device

For the second example we are adding events and a plain device. An event allows communication from the resource back to the user. The cisstMultiTask implementation of events is based on commands, i.e. the observer needs to provide a command to be called when the event occurs (this command corresponds to a callback method). The required steps are:

  1. Declare the event on the resource side and associate it to a provided interface. This step creates a command which must be used by the resource programmer when he or she desires to throw that event.
  2. Create a callback method on the user side and add it to a given required interface as an event handler command.
  3. When the tasks are connected, the system will automatically bind the user event handler to the event.

Also added in this example is a device (mtsDevice), i.e. a wrapper for an existing resource which will have an interface similar to a mtsTask. For this example we used the system clock as our resource. It is important to understand that since we don't create a thread for this device, there is no thread safety mechanism added by cisstMultiTask. This example remains safe as long as the newly created device is used by one and only one task.

Finally this example shows how to save or log some of the data stored in the state table using the mtsCollectorState. This class allows to select which ``signal'' to save (i.e. column of the state table) and define a subsampling (save all or every so often sample). The data is stored in a file in binary or text mode (comma or space separated) when everytime the state stable fills up. As this is performed in a separate thread, the data collection has a limited impact on the application.

4.1 Sine wave generator task

The class declaration is the same as in the first example except for the declaration of some data members:

[firstline=24,lastline=29, basicstyle=, caption=sineTask.h: event data members] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example2/sineTask.h

The trigger value will be set by the user and the mtsFunction TriggerEvent will be used to send the event.

[firstline=10,lastline=27, basicstyle=, caption=sineTask.cpp: constructor with event] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example2/sineTask.cpp

In the constructor, we used a nested call of Bind and AddEventWrite. The AddEventWrite add the event name to the interface and creates a multi-cast command (internally, an object of type mtsMulticastCommandWrite). It is important to note that the user needs to provide the name of the command as well as the type of object carried with the event (also known as payload). In cisstMultiTask, the type is defined using an object of the desired type. So far we didn't need to provide an object to define the parameters type as all the commands were based on state data (which is already typed).

A user task willing to receive the event will have to register its callback command (event handler). Doing so, the event handler will be added to the list of commands to ``multi-cast'' the event to.

The method AddEventWrite returns a pointer on a write command. As manipulating commands requires to call their Execute method, an mtsFunction can be used to get a more C/C++ looking code.

[firstline=41, lastline=55, basicstyle=, caption=sineTask.cpp: Run method with event] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example2/sineTask.cpp

In the Run method, we can use our mtsFunction TriggerEvent to generate an event and send the current data along.

4.2 Clock device

A cisstMultiTask device is a wrapper meant to give the appearance of a cisstMultiTask to an existing task or device. For this example we use a resource available on most systems, the clock. Compared to mtsTaskPeriodic, mtsDevice doesn't have state data nor state table nor the methods Run, Startup and Cleanup.

[firstline=11, lastline=24, basicstyle=, caption=clockDevice.h] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example2/clockDevice.h

Internally, we used the class osaStopwatch to provide an operating system independent clock.

[firstline=10, lastline=21, basicstyle=, caption=clockDevice.cpp] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example2/clockDevice.cpp

To be used like a cisstMultiTask task, our device simply needs to create a provide interface (``MainInterface'') and add commands to it (e.g. ``GetTime'') relying on existing methods (e.g. clockDevice::GetTime).

4.3 Display task

The display task for this example is very similar to the previous one. For the GUI, used FTLK/fluid to add a text widget to display the time. In the header file we need to add an mtsFunction to bind to the clock ``GetTime'' command and declare an event handler (trigger event) associated to the interface used for the data source (e.g. sine wave generator).

[firstline=22, lastline=35, basicstyle=, caption=displayTask.h: with clock device] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example2/displayTask.h

In the implementation, the constructor has been updated to register the event handler and add one required interface for the clock:

[firstline=14, lastline=28, basicstyle=, caption=displayTask.cpp: constructor with clock device] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example2/displayTask.cpp

The last parameter used for AddEventHandlerWrite determines if the event command is queued or not. If the event is queued, the associated callback will have to be dequeued by the user task itself and it will run in the user thread space. If the event command is not queued, it will be executed immediately, in the resource thread. This means that the callback implementation (e.g. displayTask::HandleTrigger) must be thread safe and not use any data member of the user task without some kind of safety mechanism (mutex, semaphore, ...). In this example, we use a non queued event handler to wake up the display task thread along with a semaphore for thread safety. Example 3 will show how to create and manage queued event handlers (both void and write events).

4.4 Main program

The main program now needs to create the clock device, add it to the task manager and make sure the device is connected to the display task.

[firstline=35,lastline=46, basicstyle=, caption=main.cpp: second example] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example2/main.cpp

Before all the tasks get started, it is possible to add a state collector to collect all or part of the data stored in the state table. This requires to first create an object of type mtsCollectorState and then specify which state data to collect (in our example, the data ``SineData'').

[firstline=51,lastline=69, basicstyle=, caption=main.cpp: second example, setting up data collection] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example2/main.cpp

The task collaboration graph is shown in figure 2.

Figure 2: Example 2: tasks graph
\includegraphics[scale=0.5]{example2}

5 Adding multiple interfaces, complex data types and qualified read commands

This example introduces:

5.1 Multiple provided and required interfaces

The class diagram for this example is shown in figure 3. The low level task, ``RobotControl'' emulates a robot controller with two arms (``Robot1'' and ``Robot2''). Each arm has two provided interfaces, a control interface which allows to move the robot as well as read the current position and stop it (sort of a read-write interface). The observer provided interface allows to read the robot position and stop it in case of emergency but it doesn't provide a command to move the robot (sort of a read-only interface).

On top of the robot controller, we are using two instantiations of a display class. The display task has two required interfaces, ``ControlledRobot'' and ``ObservedRobot''. When the two display objects are connected to the robot controller, we simply swap which required interface is connected to which provided one.

The last task, the safety ``Monitor'' shows that multiple tasks can use the same provided interface without losing thread safety.

Figure 3: Example 3: tasks graph
\includegraphics[scale=0.5]{example3}

5.2 Complex data types

So far the data types we used were simple types as defined by mtsDouble, mtsULong, ... In general one will need more complex types such as classes possibly containing pointers corresponding to dynamic memory allocation. For these types, a shallow copy (C++ default copy constructor) might not work as memory needs to be allocated for each newly created object. When a new type is to be added, the programmer must make sure that it is derived from mtsGenericObject and the copy constructor is correct (i.e. performs a deep copy if needed).

Internally, cisstMultiTask makes copies of objects for the state table or the queues of parameters (void and write commands). When using a complex type, the cisstMultiTask methods allow to provide a sample (also called argument prototype) which will be used to create all internal copies (for the curious programmers, we use the ``in-place'' copy constructor). In this example, we first declare the state data object in the header file:

[firstline=19, lastline=28, basicstyle=, caption=robotLowLevel.h] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example3/robotLowLevel.h

The data type mtsDoubleVec is equivalent to mtsVector<double> which is derived from both mtsGenericObject and vctDynamicVector. For more details on vctDynamicVector, consult the cisstVector quickstart.

In the constructor, we first need to ``configure'' the argument prototypes. In this case we need to set the size correctly using the SetSize method inherited from vctDynamicVector.

We can then add the multiple provided interfaces and commands using our robot joint types with the correct size.

[firstline=11, lastline=72, basicstyle=, caption=robotLowLevel.cpp: constructor] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example3/robotLowLevel.cpp

5.3 Using the state table index

In the previous listing, we also added a read command named ``GetStateIndex'' based on the state table method GetIndexReader. So far we used the AddReadCommandToTask method to make state data readable via an interface. This provides a convenient and easy way to read the latest available data in the table, but this will not suffice for all users. Instead, one might want to retrieve older data or make sure the data is all synchronized. In this case, sequential collection of the latest available data might not work as the underlying task could have incremented its counter. In this example, we decided to retrieve the latest data as well as the prior data using the state index.

[firstline=14, lastline=29, basicstyle=, caption=monitorTask.h] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example3/monitorTask.h

In the class declaration, note that we are using the same type for the robot joints and declared a mtsFunctionQualifiedRead which will allow us to specify the state index when retrieving the data from the state table.

The Run method can now retrieve the state index and use it to get the data from the state table by index: [firstline=36, lastline=54, basicstyle=, caption=monitorTask.cpp: Run method] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example3/monitorTask.cpp

5.4 Main program

When connecting the tasks, note that the required interfaces are swapped when connected to the provided interfaces of the robot controller:

[firstline=33, lastline=50, basicstyle=, caption=main.cpp: third example] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example3/main.cpp

6 Using a task or device without creating a new task

In the previous examples we always created a task for the user interface. This is not a requirement and it is possible to use a task from a plain function (i.e. main). We can list a few reasons to not use a task for the user interface (or any other ``top task''):

On the other hand, the class mtsTaskPeriodic or mtsTaskContinuous provides a required interface and can be connected easily using the task manager. If one decides to not use a task, these features have to be implemented by hand. It is very important to note that some built-in thread safety mechanisms are being by-passed (e.g. mailboxes for queued events) and the programmer will have to pay special attention to critical sections.

In this example we are going to use the exact same robot task as before. The user interface will be simpler in that we are only going to use one of the robot's arms.

6.1 User interface

Using the task manager to connect tasks allows to retrieve commands from the resource provided interface and subscribe to events. Since we are not using the task manager, we need to re-implement these steps. We first need to declare some function objects and create the callbacks used for the events:

[firstline=28, lastline=37, basicstyle=, caption=userInterface.h: some data members] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example6/userInterface.h

In the constructor, we assume that we already have a pointer on the correct provided interface. We then need to retrieve the commands by name, create commands for our event callbacks and then add them as observers.

[firstline=24, lastline=43, basicstyle=, caption=userInterface.cpp: constructor] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example6/userInterface.cpp

Note that it is possible to get a print-out of the current configuration of a task or interface using the stream out operator («). This includes the list of registered observers for each event as well as all the available commands.

One important thing to note in this example is that the callbacks associated to the underlying task events are not queued. This means that when the event will occur at the task level, the callback will be called by the task itself in its own thread. As our callbacks modify the user interface which is also manipulated by the main thread, it is important to use a mutex for all the UI calls to ensure thread safety.

[firstline=95, lastline=113, basicstyle=, caption=userInterface.cpp: Event callback methods] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example6/userInterface.cpp

The update method looks a lot like the Run method of the previous examples except that we don't process commands nor events:

[firstline=115, lastline=127, basicstyle=, caption=userInterface.cpp: Update method] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example6/userInterface.cpp

6.2 Main program

The main program creates the robot task, looks for a robot provided interface named ``Robot1'' and then creates the user interface. Once the user interface is created, it loops as long as the close button has not been pressed. In the loop, we periodically call the user interface Update method.

[firstline=25, lastline=53, basicstyle=, caption=main.cpp: fourth example] /home/stomach/dart/doc/source/examples/multiTaskTutorial/example6/main.cpp

7 Lists of figures and listings


List of Figures