.NET Framework Parallel Programming Design Patterns

Physics and new microprocessor architectures are forcing .NET developers to tackle Parallel Programming and rethink how they build responsive applications. My previous article, New C# and VB.NET Async language features, built on the Task Parallel Library (TPL) will eventually supplant the exiting Asynchronous Programming model and TPL will permeate all areas in the .NET Framework.

.NET developers trying to understand the new Parallel Programming classes and concepts face some challenges. For many .NET developers the greatest challenge is understanding conventions and patterns around the new technologies.

There is a buffet of Parallel Programming patterns and conventions documentation. A good approach to digesting all the available information is to ingest a little bit at a time. Developers looking for a Parallel Programming Patterns and Conventions appetizer will find it in this article.

Starting with Patterns

Narrowing a TPL Pattern introduction is not easy. Applications can leverage TPL in a variety of ways and application architecture can assume many forms. Still there is a core set of patterns that a developer will consider no matter the architecture. Below is a summary of the core patterns. Each will be examined in this article.

  • Shared State is the Parallel Pattern most developers are already familiar with. Concurrency requires disciplined access to shared data structures.
  • Parallel Loops are an easy way to leverage the Task Parallel Library. Parallel loops look a lot like regular loops. As you may have guessed there are differences, and usage requires understanding the differences.
  • Producer/Consumer is a pattern most developers will be familiar with when introduced to it.B Some objects create other objects and some objects consume objects (some objects do both). Often operating on objects or producing objects will leverage components like the .NET ThreadPool. Aside from components for executing work, TPL includes data structures for mediating between producers and consumers.

When tapping Concurrency for the first time, Shared State is the first pattern most developers will deal with so it’s the first Pattern covered.

Shared State

As mentioned earlier, concurrency requires disciplined access to shared data structures. There are three basic ways a developer works with shared data structures.

Synchronization is managing access using things like mutexes or monitors. The sample code below demonstrates one of these techniques.

var shared = new SharedOne();
shared.Val = 0;

var t1 = Task.Factory.StartNew(() =>
    {
        while(true )
        {
            lock (shared)
            {
                Console.WriteLine("t1 " + shared.Val.ToString());
                if (shared.Val > 20) { break; }
                shared.Val = shared.Val + 1;
            }

            Thread.Sleep(1000);
        }
    }
);

var t2 = Task.Factory.StartNew(() =>
{
    while (true)
    {
        lock (shared)
        {
            Console.WriteLine("t2 " + shared.Val.ToString());
            if (shared.Val > 20) { break; }
            shared.Val = shared.Val + 1;
        }

        Thread.Sleep(1000);
    }
}
);

Task.WaitAll(new Task[] { t1, t2 });

Immutability is somewhat new to.NET developers. Most developers are accustomed to passing references around an application. Truly immutable data never changes once it is initialized and references are avoided. Instead, operating on data structures requires making or cloning a copy. Immutable classes work a lot like the String class. String classes are mostly immutable. String methods return a copy of a completely different string rather than a mutated version of the existing string.

Synchronization and immutability can be thought of as the two extremes. Isolation exists between the two. There are a number of ways a developer can implement isolation. One common way is to take an Agent based approach to Concurrency and pass messages (chunks of data) between Agents executing their work on a ThreadPool. Task Parallel Library Dataflow is a library suited to implementing the message/Agent based approach to isolation.

As you may have guessed there are tradeoffs between each approach. Synchronization can create contention and bottlenecks in concurrent code. Immutable data structures may not be practical. Isolation can be hard to achieve.

A second common .NET Task Parallel Library pattern is Parallel Loops.

Parallel Loops

Some Parallel loop sample code appears below.

int upper = 100;

Parallel.For(0, upper, n =>
    {
        Thread.Sleep(n * 10);
        Console.WriteLine("n == " + n.ToString());
    }
);

Console.WriteLine("Press any key to continue...");
Console.ReadKey();

For developers who are new to the Task Parallel Library, parallel loops are a good place to start introducing concurrency. As you can see, the coding conventions are already familiar. The most important part of understanding Parallel loops is understanding that each iteration must be independent.

Each iteration is dispatched onto the ThreadPool much like the graphic below depicts.

Looping iterations
Figure 1: Looping iterations

When running the sample code above, one surprising outcome is the output doesn’t execute in an ordered fashion. Later parts execute before earlier parts. Yet, the loop doesn’t terminate until all iterations have executed.

Parallel Loops fall under another pattern category called Fork/Join.

By far, the most common TPL Pattern is the Producer/Consumer.

Producer/Consumer

Producer/Consumer assumes many faces; though every implementation will follow these guidelines.

  • A class or group of classes creates data structure instances.
  • Another class or group of classes consumes instances.
  • Some form of channel or mediation exists between producers and the consumers. Often the mediation is a queue-based data structure.

The sample below demonstrates the Producer/Consumer pattern using a BlockingCollection class.

var product = new BlockingCollection<int>();

var producer = Task.Factory.StartNew(() =>
{
    int curVal = 0;
    while (true)
    {
        Console.WriteLine("Produced " + curVal.ToString());
        if (curVal > 20) { break; }
        curVal = curVal + 1;
        product.Add(curVal);
        Thread.Sleep(500);
    }

    product.CompleteAdding();//To signal the consumer
}
);

var consumer = Task.Factory.StartNew(() =>
{
    foreach(var val in product.GetConsumingEnumerable())
    {
        Console.WriteLine("Consumed " + val.ToString());
    }
}
);

Task.WaitAll(new Task[] { producer, consumer });

Pipelines (another pattern) are often a series of Producer/Consumers strung together. I always think of Pipelines and the Producer/Consumer as a sort of assembly line. Producer/Consumer is probably the most common TPL pattern. For example: the ThreadPool/TaskScheduler is implemented using a Producer/Consumer pattern.

Conclusion

Shared State, Parallel Loops, and Producer/Consumer Patterns are just a few Parallel Patterns that leverage the Task Parallel Library. This article gives a developer a taste of the Patterns that must be mastered. For further study, delve into the resources at the end of this article.

Resources

Parallel Computing Development Web site

Technical Articles on MSDN

“Patterns of Parallel Programming: Undersanding and appliying Parallel Patterns with the .NET Framework 4.0 and Visual C#”

“Parallel Programming with .NET – Patterns and Practices”

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read