Handling Concurrency Exceptions
Parallel extensions always throw a single AggregateException
object. AggregateException
contains an inner collection property named InnerExceptions
. If individual exceptions were thrown, then it would be easier for programmers to miss one or the other possible catch
blocks. However, using a single exception class makes debugging a little harder because the break happens at the location of the AggregateException catch
block rather than the location of the underlying exception.
The net result is that debugging exceptions require you to do some extra detective work by examining the AggregateException.InnerExceptions
collection. Sometimes you can navigate the stack trace to figure out where things went wrong, but the Visual Studio call stack is not 100-percent navigable.
Tips for Enhancing Performance
I just finished re-reading Dan Brown's Digital Fortress, which is about the NSA, cryptography, spies, and secrets. The book includes some examples of simple word ciphers and references to the World War II Enigma cipher machine. This led me to Listing Two which reads the text of a book from Gutenberg.org and uses a simple cipher to encrypt the text. The sample text is from Lewis Carroll's Alice's Adventures in Wonderland and uses a simple substitution cipher to encrypt the text. At 29,000 words, the document is not huge, but if you run the sample code, you see the parallel encryption loop is significantly faster than the sequential loop.
Listing Two uses the WebClient
class to download the text of Alice in Wonderland (referring to the Cypher.Demo
method). The text of the story is split into lines -- characters actually resulted in poorer performance than the sequential version--and encrypted. The text is encrypted three times sequentially and three times in parallel--referring to the for loop and Cy pher.ScrambleSequentially
and Cypher.ScrambleParallel
methods. Performance improves significantly after ScrambleParallel
is called the first time; this is probably due to how long it takes to queue worker threads the first time. The text is encrypted and a Stopwatch
is used to display the elapsed time.
Listing Two
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.Diagnostics; using System.Threading; using System.Collections; namespace SimpleCypher { // convert to simple cypher demo - encrypting the message class Program { static void Main(string[] args) { Cypher.Test(); Cypher.Demo(); } } public class Cypher { public static void Demo() { WriteHeader("Scramble Sequential and Parallel Times"); string text = new WebClient().DownloadString( "http://www.gutenberg.org/files/11/11.txt"); // Alice's Adventures in Wonderland string[] lines = text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); CreateNewCypher(); for (int i = 0; i < 3; i++) { Stopwatch watch = Stopwatch.StartNew(); var encrypted1 = Cypher.ScrambleSequentially(lines); watch.Stop(); Console.WriteLine("Elapsed sequential: {0}", watch.Elapsed); // encrypt parallel watch = Stopwatch.StartNew(); var encrypted2 = Cypher.ScrambleParallel(lines); watch.Stop(); Console.WriteLine("Elapsed parallel: {0}", watch.Elapsed); Console.WriteLine(); } Console.ReadLine(); } private static void WriteHeader(string header) { Console.WriteLine('+' + new string('-', 40)); Console.WriteLine('|' + header); Console.WriteLine('+' + new string('-', 40)); Console.WriteLine(); } public static void Test() { WriteHeader("Scramble/Unscramble Test"); string text = new WebClient().DownloadString( "http://www.gutenberg.org/files/11/11.txt"); // Alice's Adventures in Wonderland string[] lines = text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); CreateNewCypher(); var encrypted = Cypher.ScrambleSequentially(lines); Array.ForEach(encrypted.Take(5).ToArray(), x => Console.WriteLine(x)); Console.WriteLine(); var decrypted = Cypher.Unscramble(encrypted); Array.ForEach(decrypted.Take(5).ToArray(), x=>Console.WriteLine(x)); Console.WriteLine(); } private static List<Character> cypher = new List<Character>(); private class Character : IComparable { public char cypher; public char ch; #region IComparable Members public int CompareTo(object obj) { return this.cypher.CompareTo(((Character)obj).cypher); } #endregion } public static void CreateNewCypher() { string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; cypher.Clear(); Random random = new Random(DateTime.Now.Millisecond); for (char c = 'A'; c <= 'Z'; c++) { char rnd = chars[(char)random.Next(0, chars.Length-1)]; chars = chars.Remove(chars.IndexOf(rnd), 1); cypher.Add(new Character() { ch = c, cypher = rnd }); } cypher.Sort(); Array.ForEach(cypher.ToArray(), x => Debug.WriteLine( string.Format("{0} = {1}", x.cypher, x.ch))); } public static string[] ScrambleSequentially(string[] lines) { return (from line in lines select Cypher.EncryptLine(line)).ToArray(); } public static string[] ScrambleParallel(string[] lines) { return (from line in lines.AsParallel().AsOrdered() select Cypher.EncryptLine(line)).ToArray(); } public static string[] Unscramble(string[] lines) { return (from line in lines select Cypher.DecryptLine(line)).ToArray(); } public static string EncryptLine(string line) { StringBuilder builder = new StringBuilder(); foreach (char ch in line.ToCharArray()) { Character character = cypher.Find(test => test.ch == Char.ToUpper(ch)); if (character == null) builder.Append(ch); else if (Char.IsLower(ch)) builder.Append(Char.ToLower(character.cypher)); else builder.Append(character.cypher); } return builder.ToString(); } public static string DecryptLine(string line) { StringBuilder builder = new StringBuilder(); foreach (char ch in line.ToCharArray()) { Character character = cypher.Find(test => test.cypher == Char.ToUpper(ch)); if (character == null) builder.Append(ch); else if (Char.IsLower(ch)) builder.Append(Char.ToLower(character.ch)); else builder.Append(character.ch); } return builder.ToString(); } } }
The EncryptLine
and DecryptLine
methods work by using the simple substitution cypher generated in CreateNewCypher
. CreateNewCypher
uses all of the uppercase letters of the alphabet and assigns one of 26 possible substitute values until all letters are exhausted. Thus, A might be randomly substituted for F, B for Z, and so on. The easiest way to decrypt is to reverse the process using the same cypher values. (Or use trial-and-error, manually, which any first-year cryptographer could do. You could probably use something like this to perplex casual snoops, but I wouldn't break any laws and encode details in an e-mail using cypher substitution -- if you know what I mean.)
Of course, not all problems are amenable for parallel execution. For instance, the cypher example didn't work so well at the character level, probably because the limited amount of work -- substituting one character at a timeÑcosts less than partitioning the data and spinning threads. You can enhance the results of parallel performance by following a few basic recommendations:
- Target computationally expensive algorithms, for example, where you are processing thousands or millions of items.
- Consider using the server garbage collection for parallel applications. Refer to the MSDN help documentation for the
gcServer.config
element. - Parallize outer but not inner loops unless the outer loop processes a few iterations and the inner loop processes many iterations--then process the inner loop but not the outer.
- Avoid enabling ordering and using order by unless absolutely necessary.
- Prefer independent loop iterations and
System.Threading.Tasks.Task
bodies instead of using synchronization. - Pay attention to the number of explicit
Task
bodies because some overhead is incurred. - And of course, this code is still CTP, so hopefully feedback and a few more developer cycles will improve performance.
It is worth noting that PLINQ is part of Parallel FX, so some of these tips apply to parallelism in general.
Conclusion
There are several samples with the Parallel FX Library, including a ray tracer and a C++ example that renders Mandelbrot Fractals. When I ran the ray tracer algorithm, the image rendered in 47 seconds sequentially and in 32 seconds in parallel.
Paul is an applications architect for EDS and author of LINQ Unleashed for C#. He can be contacted at [email protected].