Guide part 1: Processes, Channels, Poison and Deadlock

Boilerplate

The bare skeleton of a new C++CSP2 program usually looks as follows:
    #include <cppcsp/cppcsp.h>
    
    using namespace csp;
    
    int WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow) //<-- For Windows
    int main(int argc,char** argv) //<-- For everything else
    {
        Start_CPPCSP();
        
        ...
        
        End_CPPCSP();
        return 0;
    }

It is assumed that all C++CSP2 function calls (such as SleepFor, and Run) take place after Start_CPPCSP() has been called, and before End_CPPCSP() is called.

In the rest of this guide, it is assumed that "using namespace csp;" has already been declared.

To compile your programs, link with the C++CSP2 library. On GCC this can be achieved by using the "-lcppcsp2" option on the command-line.

Processes

Process are the central concept of C++CSP2. Processes are self-contained active pieces of code. Various analogies can be attempted (e.g. processes are active objects, processes are like threads), but it is perhaps best illustrated by example. A process in C++CSP2 is typically a subclass of csp::CSProcess:
    class WaitProcess : public CSProcess
    {
    private:
        Time t;
    protected:
        void run()
        {
            SleepFor(t);
        }
    public:
        WaitProcess(const Time& _t) : t(_t)
        {
        }
    };

It should be obvious that the behaviour of this process is to wait for a specified amount of time. A process on its own is merely a declaration -- it needs to be run. Running one process is straightforward:

    Time t = Seconds(3);
    Run(new WaitProcess(t));

The overall effect of running the process will be to wait for three seconds.

Running a single process is of limited use. Processes can be composed -- either sequentially or in parallel. Sequential and parallel composition are just as easy as each other:

    Run(InSequence
        ( new WaitProcess(Seconds(3)) )
        ( new WaitProcess(Seconds(3)) )
    );
    
    Run(InParallel
        ( new WaitProcess(Seconds(3)) )
        ( new WaitProcess(Seconds(3)) )
    );

The effect of the first csp::Run call is to wait for six seconds (wait for three seconds, then for another three). The effect of the second is to wait for three seconds (wait for three seconds in parallel with an identical wait).

As you would expect, when processes are run in sequence, each process is only started when the process before it finishes. When processes are run in parallel, all the processes are started at once, and the csp::Run call only returns when all the processes have finished.

These sequential/parallel compositions can be composed together, to any depth:

    Run(InSequence
        (InParallel
            ( new WaitProcess(Seconds(1)) )
            ( new WaitProcess(Seconds(2)) )
        )
        ( new WaitProcess(Seconds(3)) )
        (InParallel
            ( new WaitProcess(Seconds(4)) )
            (InSequence
                ( new WaitProcess(Seconds(5)) )
                ( new WaitProcess(Seconds(6)) )
            )
        )
    );

Channels

So far, the processes we have been using worked in isolation. This is quite rare -- usually the processes will need to interact. The most common need is to pass data between processes. The design philosophy behind CSP systems is that processes should not directly share data. This can lead to problems when two parallel processes both try to change data at the same time. Instead, no data should be shared, and channels should be used to pass data between processes.

Imagine that we had a simple process that continually writes increasing integers to the screen:

    class IncreasingNumberPrinter : public CSProcess
    {
    protected:
        void run()
        {
            for (int i = 1; ;i++)
            {
                std::cout << i << std::endl;
            }
        }
    };

Although it seems trivial (most simple examples are...), we could split this process in two; one that prints numbers to the screen, and another that produces increasing numbers. The two processes could then be connected by a channel. This is how we would write the number-producing process:

    class IncreasingNumbers : public CSProcess
    {
    private:
        Chanout<int> out;
    protected:
        void run()
        {
            for (int i = 1; ;i++)
            {
                out << i;
            }
        }
    public:
        IncreasingNumbers(const Chanout<int>& _out)
            :   out(_out)
        {
        }
    };

The above process will produce a stream of increasing integers on its output channel. Next we need its companion process:

    class NumberPrinter : public CSProcess
    {
    private:
        Chanin<int> in;
    protected:
        void run()
        {
            while (true)
            {
                int n;
                in >> n;
                std::cout << n << std::endl;
            }
        }
    public:
        NumberPrinter(const Chanin<int>& _in)
            :   in(_in)
        {
        }
    };

Now that we have these two processes, we need to run them both, and "plumb" the two channel-ends together using an actual channel:

    One2OneChannel<int> c;
    Run(InParallel
        ( new IncreasingNumbers( c.writer() ) )
        ( new NumberPrinter( c.reader() ) )
    );

These processes will have the same effect as our original IncreasingNumberPrinter process. But now we can change the behaviour of the system without changing our new IncreasingNumbers and NumberPrinter processes. Imagine that we now wanted to print out all the triangular numbers. We could use this process:

    class Accumulator : public CSProcess
    {
    private:
        Chanin<int> in;
        Chanout<int> out;
    protected:
        void run()
        {
            int total = 0;          
            while (true)
            {
                int n;
                in >> n;
                total += n;
                out << total;
            }
        }
    public:
        Accumulator(const Chanin<int>& _in,const Chanout<int>& _out)
            :   in(_in),out(_out)
        {
        }
    };

This process takes in numbers, and sends out the running total. If this is combined with our earlier two processes, it will start sending out the triangular numbers:

    One2OneChannel<int> sequentialNumbers;
    One2OneChannel<int> triangularNumbers;
    Run(InParallel
        ( new IncreasingNumbers( sequentialNumbers.writer() ) )
        ( new Accumulator( sequentialNumbers.reader(), triangularNumbers.writer() ) )
        ( new NumberPrinter( triangularNumbers.reader() ) )
    );

This illustrates -- in a very small way -- the power of processes. Their channels are their interface in much the same way that a function list represents the interface of a class (assuming all data is private). The processes here do not need to know what is at the other end of the channel; they just read and write numbers to their channel-ends, and then we have composed these processes together into a meaningful chain.

Poison

You may have noticed that our simple processes run forever. This is only acceptable for a small subset of programs; most programs are intended to run for a set duration and then finish. One way to achieve this would be to fix a duration in each process. They could each take a parameter to their constructor to specify how many numbers they should produce/process before finishing. This method would be annoying to program and would not scale well. If we later decided that to add a filtering process that masked out the odd triangular numbers, we'd have to change various limits differently. There are many other settings where running for a set number of iterations would not be applicable (for example, processing GUI events).

Sending a special "terminator" value would be another solution, but that does not always fit either. If a process at the end of a chain (in the above case, NumberPrinter) wants to tell the others to quit, it cannot (without extra, ugly "quit" channels being added in the opposite direction).

C++CSP2 therefore has the notion of poisoning channels. Once a channel is poisoned, it forever remains so (there is no antidote!). Poisoning can be done by either the reader or writer (poison can flow upstream). If an attempt is made to use a poisoned channel, a csp::PoisonException will be thrown. The processes can be rewritten to use poison as follows:

    class Accumulator : public CSProcess
    {
    private:
        Chanin<int> in;
        Chanout<int> out;
    protected:
        void run()
        {
            try
            {
                int total = 0;
                while (true)
                {
                    int n;
                    in >> n;
                    total += n;
                    out << total;
                }
            }
            catch (PoisonException&)
            {
                in.poison();
                out.poison();
            }
        }
    public:
        Accumulator(const Chanin<int>& _in,const Chanout<int>& _out)
            :   in(_in),out(_out)
        {
        }
    };

The easiest place to put the catch statement for poison is at the outermost layer of the run() function. The usual way to handle poison is to poison all of the process's channels and then finish (by returning from the run() function). Poisoning an already-poisoned channel has no effect (and will never result in a second PoisonException being thrown) so regardless of whether it was "in" or "out" that was poisoned, both of them are poisoned and then the process finishes.

The initial poison is spread using the poison() method on a csp::Chanin or csp::Chanout. Poison naturally spreads throughout a process network along the channels, which will usually easily cause all processes to finish.

Deadlock

Deadlock is literally when there are no C++CSP2 processes to run. This usually occurs because processes are all waiting for each other to do perform different actions. For example, consider two processes that you want to send numbers back and forth between them:
    class NumberSwapper : public CSProcess
    {
    private:
        csp::Chanin<int> in;
        csp::Chanout<int> out;
    protected:
        void run()
        {
            int n = 0;
            while (true)
            {
                out << n;
                in >> n;
            } 
        }
    public:
        NumberSwapper(const Chanin<int>& _in,const Chanout<int>& _out)
            :   in(_in),out(_out)
        {
        }
    };

Once you have written this process, you connect two of them together:

    One2OneChannel<int> c,d;
    Run(InParallel(
        ( new NumberSwapper( c.writer(), d.reader() ) )
        ( new NumberSwapper( d.writer(), c.reader() ) )
    ); //DEADLOCK!

The outcome is deadlock. Both processes start by trying to write on their channel. The process on the other side is also trying to write, so neither of them read a value, and hence both processes are forever stuck. With no processes left that can do anything, we are in a state of deadlock.

In the above scenario, this could be solved by making one process write-then-read, and the other read-then-write. In other cases the cause may be subtler than this (it may also involve barriers, which will be introduced later on).

When deadlock is encountered, a csp::DeadlockError is thrown in the original process (which will usually be in the main() function), that first called csp::Start_CPPCSP(). This is not an exception to be dealt with casually -- deadlock is a terminal program error in much the same way as a memory access violation, and it represents a latent error in your program. You may handle it to perform emergency clear-up, but it indicates that the design of your program is broken.

Whenever you use barriers, or build a process network that contains a cycle of channels, consider whether deadlock could occur.

This guide is continued in Guide part 2: Alternatives and Extended Input.


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