Guide part 5: Mobiles, Barriers and Buckets

Mobiles

csp::Mobile is a pointer-like class for implementing transfer of ownership. They function in a similar manner to std::auto_ptr, boost::shared_ptr and the like; they can be easily used in place of pointers, but with different behaviour.

When a csp::Mobile is assigned to (or communicated over a channel), the pointer transfers -- the destination receieves the new pointer, and the source is blanked. This allows pointers to be communicated around without introducing aliasing. Mobiles are used for efficiency (sending a pointer to an object is almost always faster than sending the obejct) while preventing aliasing (so that two processes do not try to write to an object at the same time) and automating deletion of the object (much as other smart pointer classes do). If you want only efficiency, send a normal pointer. If you don't want to prevent aliasing, use a boost::shared_ptr.

Examples of the use of Mobiles is provided in the next section on barriers.

Barriers

Whereas channels combine synchronisation with data-passing, barriers are used purely for synchronisation. A csp::Barrier keeps track of processes that are enrolled on it. Processes may dynamically enroll (csp::BarrierEnd::enroll() and resign (csp::BarrierEnd::resign()) at any time. A synchronisation completes when all currently-enrolled processes synchronise on the barrier (by calling csp::BarrierEnd::sync()).

Barriers (csp::Barrier) are accessed by using barrier ends (csp::BarrierEnd). The direct function of a csp::Barrier is simply to get new barrier-ends -- much as the direct function of channels is to get channel-ends. In C++CSP2, you are allowed to copy channel-ends as you wish; the only restriction is that you ensure that non-shared channel-ends are not used in parallel. Barrier-ends are more difficult, because they effectively have state -- that is, a barrier-end may or may not be enrolled. If they could be copied, what should the effect of copying an enrolled end be? To avoid such complications, BarrierEnds are prevent from being copied and are wrapped inside mobiles.

This is perhaps best explained with some code:

    Barrier barrier;
    Mobile<BarrierEnd> endA,endB;
    //Now: endA is blank, endB is blank
    endA = barrier.end();
    endB = barrier.enrolledEnd();
    //Now: endA has a non-enrolled end, endB has an enrolled end
    endA->enroll();
    endB->resign();     
    //Now: endA has an enrolled end, endB has a non-enrolled end
    CSProcess* process = new csp::common::BarrierSyncer(endB);  
    //Now: endA has an enrolled end, endB is blank
    endB = endA;
    //Now: endA is blank, endB has an enrolled end

There is one main rule to be observed:

Dynamic enrollment and resignation is useful, but it can complicate reasoning about barriers:

    Barrier barrier;
    ScopedForking forking;
    Mobile<BarrierEnd> endA,endB;
    endA = barrier.enrolledEnd();
    endB = barrier.enrolledEnd();
    forking.fork(new csp::common::BarrierSyncer(barrier.enrolledEnd());     
    //BarrierSyncer could not complete sync yet - waiting for two other processes
    endA->resign();
    //BarrierSyncer could not complete sync yet - waiting for one other process
    endA->enroll();
    endB->resign();
    //BarrierSyncer could not complete sync yet - waiting for one other process 
    endA->resign();
    endB->enroll();
    //BarrierSyncer could complete its sync between the above two statements -- it was the only process enrolled

Usually, you will find that you want all processes to enroll on the barrier to begin with, and to avoid over-use of dynamic enrollment and resignation. There is another issue to beware of. Consider the following code:

    Barrier barrier;
    Run(InParallel
        ( InSequence
            ( new csp::common::BarrierSyncer(barrier.end()) ) //A
            ( new MyProcess() )
        )
        ( InSequence
            ( new csp::common::WaitProcess(Seconds(1)) )
            ( new csp::common::BarrierSyncer(barrier.end()) ) //B   
        )
    );

Consider how long it will be before MyProcess runs. At first glance, your guess may be one second. However, the barrier-ends passed to the two csp::common::BarrierSyncer processes are non-enrolled. This means that what will likely happen is that the BarrierSyncer labelled "A" above will run, enroll (the only process then enrolled), synchronise immediately, resign and then MyProcess will be run. Around a second later, BarrierSyncer "B" will similarly enroll, synchronise immediately, and then resign. To get the behaviour that was probably desired, make sure to pass pre-enrolled ends:

    Barrier barrier;
    Run(InParallel
        ( InSequence
            ( new csp::common::BarrierSyncer(barrier.enrolledEnd()) )
            ( new MyProcess() )
        )
        ( InSequence
            ( new csp::common::WaitProcess(Seconds(1)) )
            ( new csp::common::BarrierSyncer(barrier.enrolledEnd()) )
        )
    );

Using the above, MyProcess will not be run for at least one second. It is not a problem that the BarrierSyncer process will then also call enroll on the barrier -- this second enroll call will have no effect. A single resign call will then resign (no matter how many times enroll may have been called already).

There is one further feature of C++CSP2 that can be of use when using barriers. It can be annoying -- especially in a long run() method, that may have exceptions being thrown -- to remember to enroll and, in particular, resign from barriers. Consider an illustrative example:

class SomeProcess : public CSProcess
{
private:
    Mobile<BarrierEnd> end;
    csp::Chanin<bool> in;
    csp::Chanout<int> out;
protected:
    void run()
    {
        end->enroll();
        
        try
        {       
            for (int i = 0;i < 100;i++)
            {
                bool b;
                in >> b;
            
                if (b)
                {
                    end->resign();
                    return;
                }
                            
                out << 7;
                
                end->sync();
            }
        }
        catch (PoisonException&)
        {
            in.poison();
            out.poison();
        }
        end->resign();
    }
public:
    // ... constructor ...
};

It would have been easy to forget the resign call before the return, or to place the resign call only in the catch handler, or only at the end of the try block. This process can be simplified using the csp::ScopedBarrierEnd class:

class SomeProcess : public CSProcess
{
private:
    Mobile<BarrierEnd> end;
    csp::Chanin<bool> in;
    csp::Chanout<int> out;
protected:
    void run()
    {
        ScopedBarrierEnd scopedEnd(end);
        //end will now be blank
        
        try
        {       
            for (int i = 0;i < 100;i++)
            {
                bool b;
                in >> b;
            
                if (b)
                {                   
                    return;
                }
                            
                out << 7;
                
                scopedEnd.sync(); //Using scopedEnd!
            }
        }
        catch (PoisonException&)
        {
            in.poison();
            out.poison();
        }       
    }
public:
    // ... constructor ...
};

csp::ScopedBarrierEnd is an RAII scoped class. The use of scoped classes is explained more in the Scoped page. A ScopedBarrierEnd enrolls on the given barrier in its constructor, and resigns from the barrier in its destructor. This makes it automatically resign when the function returns, or when an exception is thrown. The main subtlety involved in their use is that the csp::Mobile<csp::BarrierEnd> that you pass to the csp::ScopedBarrierEnd constructor will be blanked once the object has been constructed. Hence all synchronise calls must be made on the csp::ScopedBarrierEnd (scopedEnd in the above example), not on the original barrier-end ("end" in the above example) which will by then be blank.

Buckets

Like barriers, buckets are another sychronisation primitive. There are two operations that can be performed on buckets :

Unlike barriers, which wait for all enrolled processes to synchronise, buckets have no notion of enrollment, and some flushes may free hundreds of processes, while other flushes may free none. This can sometimes lead to non-determinism:

    Bucket bucket;
    Run(InParallel
        (new BucketJoiner(&bucket))
        (new BucketJoiner(&bucket))
        (new BucketFlusher(&bucket))
    );

The above code may, or may not, deadlock -- depending on how the processes happen to get scheduled. If both the joiners are scheduled before the flusher, the code will finish. If one (or both) joiner runs after the flusher, they will wait forever on the bucket. You should bear this consideration in mind when using buckets in your application.

This concludes the guide to C++CSP2.


Generated on Mon Aug 20 12:24:28 2007 for C++CSP2 by  doxygen 1.4.7