Chapter 3. How to create your thread-safe analysis pipeline

Writing multithreading programs is not trivial. Problems like data races (i.e. concurrently writing and reading of data), minimize explicit sharing of writable data (i.e. if you share data, it should be constant), and synchronization between tasks are only several of the things to keep in mind. For this reason, I provide two basic skeletons for analysis pipeline (one for VMUSB events and one for DDAS built events) to guide the use to a correct way to program for Parallel SpecTcl.

To start with, obtain the Parallel SpecTcl skeleton. This consists of the files in the VMUSBSkel (or DDASSkel) directory of the Skeleton installation tree. Put these files in an empty directory. For example: suppose SpecTcl is installed in /usr/opt/spectcl/6.0-000,

3.1. Parallel VMUSBSpecTcl

Example 3-1. Obtaining the Parallel SpecTcl skeleton


	    mkdir parallel
	    cd parallel
	    cp /usr/opt/spectcl/6.0-000/VMUSBSkel/* .
	  

The skeleton consists of several files; Makefile and MySpecTclApp.cpp, CMyProcessor.cpp (which is what you want to modify for your needs) and other files that you won't need to edit.

In editing MySpecTclApp.cpp, you need to consider three things:

To setup the event processing pipeline you'll need to provide #include directives to pull in the headers for your event processors. Don't specify absolute paths, take care of that in the Makefile.

Here's an example

Example 3-2. Including event processor headers


	    ...
	    //  Here you should include your headers for your event processors.
	    
	    #include <CMyProcessor.h>
	    ...
	  

The command shown is in the skeleton to indicate where to put these #include directives.

Next, locate the class MySpecTclApp, create and register the event processor(s) you need in the order you want them called. For example,

Example 3-3. Registering your event processing pipeline


	    ...
	    void
	    CMySpecTclApp::CreateAnalysisPipeline(CAnalyzer& rAnalyzer)
	    {
	      RegisterEventProcessor(*(new CStackUnpacker), "adc-data");
	      RegisterEventProcessor(*(new CMyProcessor), "example");
	    }
	    ...
	  

FYI, CStackUnpacker is the basic unpacking class for VMUSBSpecTcl that has to be defined before any user-defined processor to work correctly. For more information on VMUSBSpecTcl please refer to the corresponding section in the SpecTcl manual.

Now let's see what the CMyProcessor class looks like.

Example 3-4. Example of CMyProcessor.h


	    #ifndef CMYPROCESSOR_H
	    #define CMYPROCESSOR_H

	    #include "EventProcessor.h"
	    #include "ThreadAnalyzer.h"
	    #include <Event.h>
	    #include <TreeParameter.h>

	    class CParameterMapper;

	    class CMyProcessor : public  CEventProcessor
	    {
	    public:

	    long var1;  (1)
	    CTreeParameter pars;
	    CTreeParameter tmp;
	    CTreeVariable vars;

	    CMyProcessor(); (2)
	    CMyProcessor(const CMyProcessor& rhs);
	    virtual ~CMyProcessor();
	    virtual CMyProcessor* clone() { return new CMyProcessor(*this); }

	    void setParameterMapper(DAQ::DDAS::CParameterMapper& rParameterMapper){}; (3)

	    virtual Bool_t operator()(const Address_t pEvent, (4)
	    CEvent&         rEvent,
	    CAnalyzer&      rAnalyzer,
	    CBufferDecoder& rDecoder,
	    BufferTranslator& trans,
	    long thread);

	    virtual Bool_t OnInitialize(); (5)

	    };

	    #endif
	  
(1)
In this section the CTreeParameter, CTreeVariable, and support variables are be defined. In the implementation file, one can see how the CTreeParameter tmp is used as copy of another CTreeParameter.
(2)
This is a very important part of the thread-safe code for the processor. We need an explicit declaration of the class constructor, copy constructor, destructor, and clone method. If one does NOT implement correctly the class, failure will occur at compilation time.
(3)
Although there is no explicit mapping declaration inside CreatePipelineAnalysis for VMUSBSpecTcl, this declaration is fundamental for the compilation of the code. For more details on the reason of its importance, come to my office and I'll explain it to you (hint: implementation of a method for a pure virtual function).
(4)
If compared to normal SpecTcl, the function operator requires extra arguments to for the proper definition of our multithreading program.
(5)
These are the classic method that SpecTcl offers OnInitialize, OnBegin, OnEnd... It's up to the user knowing if the analysis code needs them or not.

Example 3-5. Example of CMyProcessor.cpp


	    #include "CMyProcessor.h"
	    #include <CRingBufferDecoder.h>
	    #include <SpecTcl.h>
	    #include <sstream>
	    #include <iomanip>
	    #include <cmath>

	    CMyProcessor::CMyProcessor():
	    var1(0)
	    {}

	    CMyProcessor::CMyProcessor(const CMyProcessor& rhs):
	    var1(rhs.var1)
	    {}

	    CMyProcessor::~CMyProcessor()
	    {}

	    Bool_t
	    CMyProcessor::operator()(const Address_t pEvent,
	    CEvent&         rEvent,
	    CAnalyzer&      rAnalyzer,
	    CBufferDecoder& rDecoder,
	    BufferTranslator& trans,
	    long thread)
	    {
	    if (tmp.isValid()){  (1)

	    pars = tmp + vars*var1;

	    }

	    return kfTRUE;
	    }

	    Bool_t
	    CMyProcessor::OnInitialize() (2)
	    {
	    tmp.Initialize("adc1.06");
	    pars.Initialize("test_var", 16384, 0.0, 16383.0, "");
	    vars.Initialize("const", 10, "");
	    if (!tmp.isBound())
	    tmp.Bind();
	    if (!pars.isBound())
	    pars.Bind();

	    return kfTRUE;
	    }
          
(1)
A simple example of parameter calibration based on a previously defined CTreeParameter (tmp), CTreeVariable (vars), and a long variable (var1).
(2)
After the Parallel SpecTcl app is built and started, a few CTreeParameter and CTreeVariable are initialized. To make the example complete, we initialized tmp as a copy of the CTreeParameter associated to the ADC channel number 6, pars as a new CTreeParameter that will correspond to some calibrated quantity, and vars as a CTreeVariable for the calibration process. Note that the two defined new CTreeParameter are bound at this stage as well.

Once you've edited everything, use make to build the shared object and don't forget to add it to the Makefile.

NB: If you have a more complex structure for your calibration parameters please look at the DDASSkel example for more details.