Introduction
This article demonstrates the use of C# anonymous delegates and a using()
statement to achieve a very simple interface to multi-threading, in a manner in akin to OpenMP in C++.
It also provides an implementation and example of a multi-threading object, avForThread
.
Background
Threading, in any language, introduces problems at all levels, from design, to implementation, to debugging. The OpenMP extension to Visual C++ allows programmers either to quickly parallelise otherwise sequential functions, or at least to test whether more specialised threading routines will be beneficial for their task. Unfortunately, currently, C# has no OpenMP extension implementation (at the time of writing, at least!)
However, C# does have several very elegant toys which can be combined to produce a coding interface very similar to that of OpenMP; primarily anonymous delegates, and using()
.
Anonymous delegates sport two great properties. One, they can be casually defined within function definitions; two, objects defined outside of the scope of the delegate, but within the defining scope, are still visible and accessible.
For example:
public delegate void testSetter(int setTo);
void myMethod()
{
int b;
testSetter D = delegate(int setTo) { b= setTo; }
D(123);
}
In this case, the delegate instance D
is set to an anonymous delegate defined inline. The delegate, in this case, sets b
to the value of the incoming parameter.
Whilst this syntax is evidently not useful in this case, it does allow the programmer a large amount of flexibility, especially when it comes to dynamic querying, or polymorphic behaviour without the need for the bloat of new classes.
One use where this syntax is very useful is in being able to wrap up functions with any number of parameters and have threads execute them.
For example, if we pass an anonymous delegate to the constructor of a System.Threading.Thread
object, we can then run the delegate with the parameters of our choosing.
So, let's say we want to take a for
loop and split it into a number of threads. (In fact, the provided code only handles for
loops, but it could be easily extended to handle any sequential mutual calculations within any looping structure - just don't write to data which may be read or written to by another thread!) As we want to use the calculated data immediately, we will wait for the threads to return.
The code attached defines all tasks needed for this kind of operations within one class, avForThread
.
To remove the need to call a method to wait for the completion of the threads, we can employ a trick with a using()
statement such that the execution will not leave the using
block until the threads have returned. This is achieved by the call to the thread wait method, avForThread.WaitToFinish()
, stated within avForThread.Dispose()
.
Using the code
So, we will start by creating a test for
loop, the kind of code that could be parallelised...
static void Main(string[] args)
{
int dataSize = 1000000;
int[] data = new int[dataSize];
for (int i = 0; i < dataSize; i++)
{
data[i] = calculateSomething(i);
}
We'll then move on to performing the same task using the threaded notation, using an avForThread
object. We will need to pass the same start and end values used by our simple for
loop. We will also pass the number of threads to use, and of course, the parameterised delegate to execute.
using (new avForThread(0, dataSize, 4,
delegate(int start, int end)
{
for (int i = start; i < end; i++)
{
data[i] = calculateSomething(i);
}
})
) ;
}
N.B.: To change the looping range, we can modify the 0
or dataSize
arguments. Only, we don't need to modify start
or end
- they will be automatically set per thread by avForThread
.
As you can see, the syntax is very simple and flexible. You will need to test with differing numbers of threads to gain the best performance. Depending on the complexity of the task and the CPU, there will be a "sweet spot" where the threaded code is far faster than the non-threaded code. Experiment with numThreads
in the range 2 to 20.
For your own implementations, you'll need to keep roughly the same format as the for
loop has here. By the way, here, the using()
simply forces the runtime to call the Dispose
method of the class, which in turn will wait for the threads to finish.
You could also omit the using
statement, carry on working, and then call the avForThread.WaitToFinish()
method explicitly.
As I said, if you want to go beyond a simple i++ for
loop, some simple customisation of avForThread
will be required.
Have fun!