Multithreaded Programming Using CSP.NET

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

This article describes how to write multithreaded programs, also known as concurrent programs, using the CSP.NET library. CSP.NET is .NET 2.0 library that provides a more intuitive and secure alternative to standard thread-programming, which is often used in concurrent programs. The library supports both distributed and concurrent programming, but the focus of this article is solely on concurrent programing. CSP.NET and documentation is available for free at www.cspdotnet.com.

Background

Many programmers know but do not use concurrent programming, hence relying primarily on sequential programming—only one thread of execution. So far, this has been an acceptable choice because of the constant increase in processor performance, but unfortunately those happy days are over. All major manufacturers of microprocessors are aiming towards multiple cores on a chip and multiple chips in a machine, but the performance of a single core will probably not increase significantly. Microprocessors containing four cores will most likely be on the market this year and Intel has announced that 32 cores will be available in 2010.

These multi-chip, multi-core machines will have a major impact on existing and future software. Concurrent programming is not an option anymore; it’s a way of life because concurrent programs are necessary in order to exploit the full power of the machines.

Traditional multithreaded programming

The vast majority of programmers use threads and various locks and synchronization mechanisms to achieve concurrency. This is a low-level approach not capable of expressing complex concurrent programs in a intuitive and simple manner. Threads are nondeterministic in nature and the lack of simplicity makes deadlocks and livelocks hard to detect. These are major drawbacks because concurrent programming is much harder than sequential programming and the main reasons for this added complexity are nondeterminism, inconsistent data, deadlocks, and livelocks, which are absent in sequential programs.

For those new to concurrent programming, a program is nondeterministic if it doesn’t exhibit unique behavior—the same input always result in the same behavior. A nondeterministic program is, of course, a lot harder to debug than a deterministic program. A program is said to deadlock if no part of the program is capable of making any progress; for example, if part A is waiting for response from part B, which is waiting for response from part C, which again is waiting for response from part A. And finally, a livelock occurs if there is an infinite internal communication between various part of the program, hence a livelock can be seen as an infinite loop.

Due to these drawbacks, the thread paradigm isn’t suited for large-scale concurrent programming. The next section introduces CSP.NET, built upon the Communication Sequential Processes paradigm, which tries to take some of the earlier mentioned difficulties into account.

Introducing CSP.NET

This sections describes how to use CSP.NET as an alternative to thread-programming described in the last sections. I’ll start out by introducing the basic concepts in CSP.NET and then move on to show an implementation of the Producer/Consumer problem using CSP.NET. A lot of CSP.NET features are left out in this section, but they are described in the CSP.NET documentation.

What is CSP.NET?

CSP.NET is a Microsoft.NET 2.0 library designed to ease distributed and concurrent programming in any language supported by the .NET platform—C#, VB, and C++. CSP.NET is a new library, but it’s built upon an old paradigm named Communicating Sequential Processes (CSP) first introduced in 1978. CSP is an algebra for describing and reasoning about distributed and concurrent programs, but you don’t need to know CSP to use CSP.NET.

A CSP.NET program essentially consists of multiple processes communicating through channels.

Processes

A process in CSP.NET is simply a class implementing the ICSProcess interface, shown below.

public interface ICSProcess
{
   void Run();
}

A very simple process writing all even numbers less than 100 is shown below.

public class Even : ICSProcess
{
   public void Run()
   {
      for(int i=2; i < 100; i += 2)
      Console.Writeline(i);
   }
}

Once the processes have been created, they may be executed by using the Parallel class. The Parallel class runs each process as an operating system thread, meaning that they may be executed in parallel on a multiprocessor machine and concurrently on single processor machines. The code below creates an instance of the Parallel class with two processes, Even and Odd, and execute the processes by calling the Run method.

Parallel par = new Parallel(new ICSProcess[]{new Even(),
                            new Odd()});
par.Run();

The Run method of the Parallel class returns when all the processes have finished running.

Channels

The last section illustrates how to create and run multiple processes concurrently, but processes often need to communicate and the way to do that in CSP.NET is through channels. Two types of channels are provided, anonymous channels, and named channels, but this article only focuses on anonymous channels.

Four kinds of anonymous channels are provided by CSP.NET, One2OneChannel, One2AnyChannel, Any2OneChannel, and Any2AnyChannel, and all of them include a Write method and a Read method for writing to and reading from the channel. Another important feature in common for all channels is that they are used by at most two processes at any given time, although multiple processes may be connected to the channel. Finally, all CSP.NET channels are so-called rendezvous channels; in other words, the Write method is blocked until another process reads the data from the channel and the Read method is blocked until data is available from the channel. If preferred, channels can be buffered, making the Write method non-blocking.

The One2OneChannel is shared by two processes, a writer and a reader. The One2AnyChannel has one writer and multiple readers. The writer writes data to the channel and one of the readers read the data. Notice that all the readers may try to read data from the channel, but one and only one of them will get the data and we don’t know which one. All we know is that one of them has read it when the Write method returns.

We do not know which reader gets the data, but we know that one and only one gets it. The Any2OneChannel is shared by multiple writers and one reader and again only one of the writers can use the channel at any given time.The Any2AnyChannel has multiple writers and readers and only one writer and one reader uses the channel at any given time.

Anonymous channels are created just like any other object.

One2OneChannel< string >  chan1 = new One2OneChannel< string  >();
One2AnyChannel< Anytype > chan2 = new One2AnyChannel< Anytype >();

Channels are generic thus any data type may be send through a channel.

A Simple Example

This section shows an implementation of a very simple problem—the Producer/Consumer problem. The producer produces some kind of items, in our case Integers, and the consumer consumes these items. Before you rush into the code, you should get CSP.NET up and running.

CSP.NET can be downloaded here and for now you only have to download the CSP.NET Library; the Name Server Service and the WorkerPool Service are used in distributed applications. Once CSP.NET is downloaded, install it and add a reference from your project to the CSP.dll. That’s it; you are ready to code.

All you need is a Consumer process and a Producer process. The Consumer process is shown below.

using System;
using System.Collections.Generic;
using System.Text;
using Csp;                                // use the Csp namespace

namespace MultiprogrammingCSP
{
   // implement the ICSProcess interface.
   class Consumer : ICSProcess
   {
   // Channel to receive Integers.
      private IChannelIn< int > inChannel;

      public Consumer(IChannelIn< int > c)
      {
            inChannel = c;
      }

      public void Run()
      {
         // Read 10 Integers from the channel and
         // write them to the Console.
         for(int i=0; i < 10; i++)
            Console.WriteLine("ttt Consumed: "+ inChannel.Read());
      }
   }

The implementation of the Consumer process is very simple. Notice that inChannel is declared as an IChannelIn channel. All channels implement the IChannelIn interface; hence, the Consumer process will accept any kind of channel. Furthermore, IChannelIn specifies that the process reads from the channel.

The Run method simply reads Integers from the channel by calling the Read method. Remember that the Read method will block until data is available.

Next, you need a producer to feed the consumer with Integers.

class Producer : ICSProcess
{
   private IChannelOut< int > outChannel;

   public Producer(IChannelOut< int > c)
   {
      outChannel = c;
   }

   public void Run()
   {
      for (int i = 0; i < 10; i++)
      {
         Console.WriteLine("Produced: " + i);
         outChannel.Write(i);
      }
   }
}

The Producer process is similar to the Consumer, but you use an IChannelOut channel instead of an IChannelIn channel because you need to write the produced items to the channel.

The processes are implemented and all you need is to get them running.

   public class ProducerConsumer
   {
      public static void Main()
      {
         CspManager.InitStandAlone();
         One2OneChannel< int > chan = new One2OneChannel< int >();
         new Parallel(new ICSProcess[] { new Producer(chan),
                      new Consumer(chan) }).Run();
      }
   }
}

The Main program calls the InitStandAlone method, which is standard procedure in every CSP.NET program; always call InitStandAlone as the first thing in your CSP.NET programs. Actually, InitStandAlone isn’t the only alternative; but, as long as your program is running in one application domain, InitStandAlone should be used.

An One2OneChannel channel is created and passed to the constructor of the producer and consumer. Finally, you create an instance of the Parallel class and run the producer and consumer processes concurrently. The Parallel.Run returns when both processes have finished.

That is all there is to it—very easy. No explicit use of threads, no need to lock shared data. It’s very simple and intuitive.

One may argue that the producer shouldn’t wait for the consumer to get an item before moving on to produce a new one. Fortunately, CSP.NET channels can be defined with buffers, thus a solution to the problem is very easy: Just use a buffered channel. Replace

One2OneChannel< int > chan = new One2OneChannel< int >();

with

One2OneChannel< int > chan =
   new One2OneChannel< int >(new FifoBuffer< int >(10));

in the Main method. You can define your own buffer or use one the predefined buffers—for example, the FifoBuffer—which is a standard first-in, first-out buffer.

There may be more than one consumer, but again this is very easy to solve. Use an One2AnyChannel and add the consumer processes to the Parallel instance, as in this example:

One2AnyChannel< int > chan = new One2OneChannel< int >();
new Parallel(new ICSProcess[] { new Producer(chan),
             new Consumer(chan), new Consumer(chan) }).Run();

I’ll leave it up to you to figure out how to handle multiple producers and consumers, but again it’s very easy.

Conclusion

This article has given a very brief introduction to CSP.NET and a lot of features haven’t even been mentioned. Those include barriers and buckets used to synchronize processes and named channels primarily used for communication between processes residing on different machines. CSP.NET also provides a DistParallel class, similar to the Parallel class, capable of running processes on remote machines. All CSP.NET features are briefly described in the documentation.

Although the program presented was very simple, it should illustrate that the CSP paradigm is more intuitive and simple than standard thread programming.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read