Guide part 4: Advanced Ways of Running Processes

Forking Processes

There will often be times where you want to start a process running, but also continue code in the current function. This may be just for ease, or you may want to interact with the processes. C++CSP2 provides a mechanism for doing this; forking. Forking is done using csp::ScopedForking, a Scoped object:
    One2OneChannel<int> c,d;
    {
        ScopedForking forking;
        
        forking.fork(new Id<int>(c.reader(),d.writer()));
        
        c.writer() << 6;
        int n;
        d.reader() >> n;
        
        c.writer().poison();
        d.reader().poison();
    }

It is very important that the channels used by the forked process were declared before the csp::ScopedForking. In C++, objects are destroyed in reverse order of their construction. So if the channels were constructed after the csp::ScopedForking, they would be destroyed before csp::ScopedForking. This means that the processes you have forked could end up using ends of channels that have been destroyed. So it is very important that you declare all channels, barriers, buckets, etc that the forked processes use before the csp::ScopedForking.

You should also be careful about poisoning channels and such. Consider this illustrative code:

    One2OneChannel<int> c,d,e;
    try
    {
        ScopedForking forking;
        
        forking.fork(new Successor<int>(c.reader(),d.writer()));
        
        forking.fork(new Widget(e.writer()));
        
        int n;
        e.reader() >> n;
        n *= -6;
        c.writer() << n;
        d.reader() >> n;
    }
    catch (PoisonException& e)
    {
        e.reader().poison();
        c.writer().poison();
        d.reader().poison();
    }   

Consider what would happen if the Widget process poisoned its channel "e" before we tried to read from it. A csp::PoisonException would be thrown. As part of the stack unwinding, the csp::ScopedForking object would be destroyed -- which would cause it to wait for all its forked processes. However, the Successor process would not have finished, because it runs until it is poisoned -- and we will not poison its channels until the catch block. So the above code will deadlock if the Widget channel turns out to be poisoned. You can avoid such problems by declaring the csp::ScopedForking object outside the try block (but after the channel declarations).

csp::ScopedForking can be used to fork compounds of processes -- anything that can be passed to the Run command can be passed to csp::ScopedForking::fork:

    forking.fork( InParallel(P)(Q) );
    forking.fork( InSequence(S)(T) );

Running Processes In The Same Thread

So far in this guide, I have tried to avoid going in to detail on how the library is implemented, to keep the introduction simple. However, this section requires such detail. When you use the csp::Run method on a single process, or csp::ScopedForking::fork on a single process, that process is run in a new (kernel-)thread of its own. Kernel-threads (usually referred to simply as threads) are OS-level concepts that are created using CreateThread on Windows, and pthread_create on Linux. Kernel-threads can be run on different processors to each other, and thus can take advantage of multi-core and multi-CPU machines. When you use code such as this:
    forking.fork( InParallel(P)(Q) );
    forking.fork( InSequence(S)(T) );
P, Q, S and T will all be run in separate threads. The advantage of using kernel-threads is that taking advantage of parallel processors is automatic. The main disadvantage of kernel-threads is their speed. If you want to perform a great deal of communication between two processes, kernel-threads can be slow. For programs where the number of processes is small (perhaps tens) this may not be a problem. However, if you are writing large-scale simulations with thousands of processes, this speed may well be an issue.

The alternative to kernel-threads is user-threads. User-threads live inside kernel-threads -- two user-threads in the same kernel-thread cannot run on separate processors in parallel. However, communication between user-threads is usually five to ten times as fast as kernel-threads. Processes can be grouped together as follows:

    forking.fork( InParallelOneThread(P)(Q) );

More detail on the threading arrangements that different combinations give are available on the Running Processes page.

This guide is continued in Guide part 5: Mobiles, Barriers and Buckets.


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