Andrew Jones
Andrew Jones

Reputation: 73

Parallel For loop in C# with shared variable

I'm trying to use parallel processing to speed up a couple of nested loops, but I'm having trouble getting the syntax right. I'm trying to get a count of how many pixels in a bitmap are either red, white, or black, the values for which I have in an enum elsewhere.

In serial processing I have the following code, which works fine:

        Bitmap image = new Bitmap(@"Input.png");
        var colourCount = new int[3];

        for (var x = 0; x < image.Width; x++)
        {
            for (var y = 0; y < image.Height; y++)
            {
                switch (image.GetPixel(x, y).ToArgb())
                {
                    case (int)colours.red: colourCount[0]++; break;
                    case (int)colours.white: colourCount[1]++; break;
                    case (int)colours.black: colourCount[2]++; break;
                    default: throw new ArgumentOutOfRangeException(string.Format("Unexpected colour found: '{0}'", image.GetPixel(x, y).ToArgb()));
                }
            }
        }

I've seen code for parallel for loops by Microsoft and from Stackoverflow that update a shared variable such as below:

        Parallel.For<int>(0, result.Count, () => 0, (i, loop, subtotal) =>
        {
            subtotal += result[i];
            return subtotal;
        },
            (x) => Interlocked.Add(ref sum, x)
        );

But all the examples use a simple type such as an int as the shared variable and I just can't figure out the syntax to write to my size three array. Am I approaching this all wrong?

By the way, I know in terms of performance that GetPixel is very slow compared to something like Bitmap.LockBits, I'm just trying to get the principle of parallel loops right.

Upvotes: 7

Views: 10472

Answers (1)

Douglas
Douglas

Reputation: 54917

You can use an overload of Parallel.For that permits you to maintain thread-local state. In this case, we create an int[3] array for each thread that is spawned. Within each iteration of the parallel loop, we only update the local array, localColourCount. Finally, when the thread is to be retired, we aggregate the results of each local array into the global one, colourCount; however, since this is a shared data structure, we enforce mutual exclusion whilst accessing it.

Bitmap image = new Bitmap(@"Input.png");
var colourCount = new int[3];

Parallel.For(0, image.Width,

    // localInit: The function delegate that returns the initial state
    //            of the local data for each task.
    () => new int[3],

    // body: The delegate that is invoked once per iteration.
    (int x, ParallelLoopState state, int[] localColourCount) =>
    {
        for (var y = 0; y < image.Height; y++)
        {
            switch (image.GetPixel(x, y).ToArgb())
            {
                case (int)colours.red: localColourCount[0]++; break;
                case (int)colours.white: localColourCount[1]++; break;
                case (int)colours.black: localColourCount[2]++; break;
                default: throw new ArgumentOutOfRangeException(
                             string.Format("Unexpected colour found: '{0}'", 
                             image.GetPixel(x, y).ToArgb()));
            }
        }
    },

    // localFinally: The delegate that performs a final action
    //               on the local state of each task.
    (int[] localColourCount) =>
    {
        // Accessing shared variable; synchronize access.
        lock (colourCount)
        {
            for (int i = 0; i < 3; ++i)
                colourCount[i] += localColourCount[i];
        }
    });

This code assumes that Bitmap.GetPixel is thread-safe, which may or may not be the case.

Another thing you need to watch out for is that any ArgumentOutOfRangeException instances will get combined into an AggregateException, so you'll need to adjust your error-handling code.

Upvotes: 6

Related Questions