This can be seen as a use of the RAII (Resource Acquisition Is Initialisation) pattern.
This idea has been used for a few C++CSP2 classes, such as ScopedForking, ScopedBarrierEnd and ScopedExtInput. When the class is constructed, the "action" begins. In the case of ScopedForking, nothing actually happens during construction. However, ScopedBarrierEnd enrolls on the barrier on construction, and ScopedExtInput begins an extended input on construction.
During the objects' lifetime, they can be used. ScopedForking can fork processes and ScopedBarrierEnd can synchronize on the barrier (ScopedExtInput has nothing like that).
On destruction the action is finished. ScopedForking's destructor waits for all the forked processes to finish. ScopedBarrierEnd resigns from the barrier. ScopedExtInput completes the extended input.
The usefulness of these objects (compared to using a normal BarrierEnd, or a beginExtInput/endExtInput function pair) is that the action is always completed and cleaned-up properly, even in the case of an exception being thrown. This means you don't have to spend effort remembering to always end an extended input (or always resign from a barrier) when faced with complex code paths; C++'s scope naturally sorts out the problem for you.
try { ScopedBarrierEnd end(barrier.end()); if (x) { if (y) { ... } else { if (z) { //No need to remember to resign from the barrier here - it will happen automatically return; } else { //No need to remember to resign here either - it happens automatically when destroyed //by the stack-unwinding caused by the exception throw Exception(); } } } } //If you reach here normally, the barrier is resigned from too catch (Exception) { }
These objects are intended for use on the stack, as above. You can use them with new/delete but you lose their benefits. Also, the objects are expected to always be used from the same process, so passing a pointer to them around could cause problems.
In particular, these objects must never be used as member of your process class. This is because the objects will be deleted by the destructor of the process, which can happen in a different process. The Scoped prefix on these classes is intended to remind the user that they are only suitable for use as local variables in functions, not as class members.
C++ local objects are destroyed in reverse order of their construction.
{
MyClass a;
MyClass b;
MyClass c;
} //First c is destroyed, then b is destroyed, and then a.
Programmers should bear this in mind when writing code. Consider the following code:
{ ScopedForking forking; One2OneChannel<int> c,d; int n; forking.forkInThisThread(new common::Id<int>(c.reader(),d.writer()); c.writer() << 6; d.reader() >> n; c.writer().poison(); }
At first glance this code looks fine. In actuality, it contains a dangerous problem. The Id process will be forked, the two communications will happen, and then this code will poison the channel c. The very next action - which will happen before Id has run again - is to reach the end of the block and begin destroying objects. d will be destroyed, then c will be destroyed, then forking will be destroyed - which will block until the Id process has finished. At this point, Id will run again. It is now using the ends of two channels that have now been destroyed! Obviously this is a problem, and could cause a crash.
The solution is to re-order the first two lines:
{ One2OneChannel<int> c,d; ScopedForking forking;
Because of this potential subtle error, I prefer to put the channel, barrier, etc declarations outside the block containing the scoped forking, to avoid having to think about construction order:
One2OneChannel<int> c,d;
{
ScopedForking forking;
...
} //Channels will definitely still exist here
Classes | |
class | csp::ScopedBarrierEnd |
A scoped Barrier end. More... | |
class | csp::ScopedExtInput< DATA_TYPE > |
Provides an easy way to perform extended inputs using scope. More... | |
class | csp::ScopedForking |
A class used to fork processes. More... |