CPP: A Tango device class with its own thread
This HowTo details how you can write a Tango class when it has to have its own thread and with data shared between the device attributes and the thread
It is relatively common to have Tango classes where you permanently have something to do. This could be implemented by running a separate thread which will do these tasks. If you also have attributes with data shared between the thread and the attributes themselves, special care has to be taken to protect the data access on both side. This HowTo gives one possible solution to this problem.
Let's suppose we have a Tango class called ThExample with two scalar read_write attributes called FirstAttr (DevLong) and SecondAttr (DevDouble). Both of these attributes used data shared with the thread.
The class is called TheThread and inherits from
The thread code is the following
On the device side, the init_device() method creates the thread and initialises the Tango attributes related data
The device delete_device() method kills the thread and is coded like the following
The Tango class read_attr_hardware() method is coded like the following
The code of the read and write method for the attributes is
The write_FirstAttr() method retrieves the value sent by the caller and writes it into the shared memory area (protected code)
This HowTo gives one solution of the problem. There are certainly some other that I let you imagine. It creates one thread by device which could not be well adapted to every kind of problems. It is also possible to create and start the thread in the ThExampleClass singleton ctor if you want only one thread whatever the device number is. On top of that, if you have many data shared between the Tango class and the thread, you can defined several mutexes which will allow you to have a finer tuning on the shared data protection.
Let's suppose we have a Tango class called ThExample with two scalar read_write attributes called FirstAttr (DevLong) and SecondAttr (DevDouble). Both of these attributes used data shared with the thread.
Here is the data added to the ThExample class
...Pogo generated code...
protected :
// Add your own data members here
//-----------------------------------------
public:
typedef struct ShData
{
double db_data;
long lo_data;
bool th_exit;
};
protected:
TheThread *th;
omni_mutex the_mutex;
ShData the_shared_data;
ShData the_local_data;
};
To store the effectively shared data, a structure called ShData has been defined with the two shared data and a boolean used to ask the thread to suicide itself. Four data members have been added to the class:
- A pointer to the thread object
- A mutex which will be used to protect the shared data access
- The shared data themselves
- A local copy of the shared data
class TheThread: public omni_thread, public Tango::LogAdapter
{
public:
TheThread(ThExample *dev,omni_mutex &mut,ThExample::ShData &dat):
omni_thread(),Tango::LogAdapter(dev),the_mutex(mut),the_shared_data(dat)
{start_undetached();}
virtual ~TheThread() {}
void *run_undetached(void *);
private:
omni_mutex &the_mutex;
ThExample::ShData &the_shared_data;
bool local_th_exit;
};
The class is called TheThread and inherits from
- the omni_thread class
- the LogAdapter class to be able to print/send Tango log messages
- The device which will be used for the Tango Log Messages
- A reference to the mutex used to protect the shared data
- A reference to the shared data structure
The thread code is the following
void *TheThread::run_undetached(void *ptr)When the thread starts, it initialise the shared data and enter an endless loop. Within this loop, it modifies the shared data and read the device suicide order flag. If the device asks for suicide it does this...If not, it sleeps and re-enter the loop. The access to the shared data is protected using the mutex received as one of the thread instance ctor.
{
DEBUG_STREAM << "The thread starts..." << endl;
//
// Init data
//
{
omni_mutex_lock lo(the_mutex);
the_shared_data.lo_data = 0;
the_shared_data.db_data = 10.0;
}
//
// An endless loop
//
while(1)
{
{
omni_mutex_lock lo(the_mutex);
local_th_exit = the_shared_data.th_exit;
the_shared_data.lo_data++;
the_shared_data.db_data++;
}
if (local_th_exit == true)
{
DEBUG_STREAM << "Arrg, I have been killed" << endl;
omni_thread::exit();
}
sleep(1);
DEBUG_STREAM << "This stupid thread has executed a new loop" << endl;
}
}
On the device side, the init_device() method creates the thread and initialises the Tango attributes related data
void ThExample::init_device()The method initialises the pointer used during the Tango attribute set_value() method to points the the shared data local copy. Then it initialises the suicide order flag to false and it creates the thread (which will also starts it)
{
INFO_STREAM << "ThExample::ThExample() create device " << device_name << endl;
// Initialise variables to default values
//--------------------------------------------
//
// Init attr related data
//
attr_FirstAttr_read = &the_local_data.lo_data;
attr_SecondAttr_read = &the_local_data.db_data;
//
// Create and start the thread
//
the_shared_data.th_exit = false;
th = new TheThread(this,the_mutex,the_shared_data);
}
The device delete_device() method kills the thread and is coded like the following
void ThExample::delete_device()This method asks the thread to exit and then waits for the thread termination in the join() method call. Note that the join() call will also reclaim the thread memory.
{
// Delete device's allocated object
//
// Ask the thread to suicide itself
//
{
omni_mutex_lock lo(the_mutex);
the_shared_data.th_exit = true;
}
void *ptr;
DEBUG_STREAM << "Waiting for the thread to exit" << endl;
th->join(&ptr);
}
The Tango class read_attr_hardware() method is coded like the following
void ThExample::read_attr_hardware(vector<long> &attr_list)This code takes the mutex control and copy the shared data within a local copy. This is necessary because when you use attribute, the data you pass to the attribute object with the set_value are given to Tango by pointer. Then, it is somewhere within the Tango layer that the data will be effectively copied to be sent on the network and you do not have a way to protect this code. Using a local copy is a safe way to return data to the client.
{
DEBUG_STREAM << "ThExample::read_attr_hardware(vector<long> &attr_list) entering... "<< endl;
// Add your own code here
omni_mutex_lock lo(the_mutex);
the_local_data = the_shared_data;
}
The code of the read and write method for the attributes is
void ThExample::read_FirstAttr(Tango::Attribute &attr)The read_FirstAttr() method is simply a call to the attribute set_value() method with the pointer sets to point to the local data copy
{
DEBUG_STREAM << "ThExample::read_FirstAttr(Tango::Attribute &attr) entering... "<< endl;
attr.set_value(attr_FirstAttr_read);
}
void ThExample::write_FirstAttr(Tango::WAttribute &attr)
{
DEBUG_STREAM << "ThExample::write_FirstAttr(Tango::WAttribute &attr) entering... "<< endl;
attr.get_write_value(attr_FirstAttr_write);
{
omni_mutex_lock lo(the_mutex);
the_shared_data.lo_data = attr_FirstAttr_write;
}
}
The write_FirstAttr() method retrieves the value sent by the caller and writes it into the shared memory area (protected code)
This HowTo gives one solution of the problem. There are certainly some other that I let you imagine. It creates one thread by device which could not be well adapted to every kind of problems. It is also possible to create and start the thread in the ThExampleClass singleton ctor if you want only one thread whatever the device number is. On top of that, if you have many data shared between the Tango class and the thread, you can defined several mutexes which will allow you to have a finer tuning on the shared data protection.