This website uses Google cookies to provide its services and analyze your traffic. Your IP address and user-agent are shared with Google, along with performance and security metrics, to ensure quality of service, generate usage statistics and detect and address abuses.More information

Ver sitio en español Go to homepage Contact me
lunes, 4 de septiembre de 2017

Multitasking I, basic classes

With this article begins a series in which I will review the basic mechanisms provided by the .NET Framework for the implementation of multitasking applications. First of all I will show the basic classes that allow you to launch multiple processes and make a performance comparison between them.

Multitasking consists of the simultaneous execution of several processes, which are also called tasks or threads. In older systems with only a single core processor, it is impossible to run two code fragments at the same time, so the system allocates time to each task and goes jumping from one to the other so it seems that they are running simultaneously. This change from one task to another involves storing the state (registers, point of execution, etc.) of the current task and retrieving the status of the task to which the control is passed. This context change has a run-time cost, so in single-processor systems, the parallel process is slower than the serial execution of the same code when it comes to performing operations that only involve processor time, such as mathematical calculations.

However, operations that may involve longer or shorter waiting times, such as reading files from disk, database queries, or downloading files from a network, are much more appropriate for the use of asynchronous operations, since, while we are downloading a file, we can use the processor to perform other operations, instead of waiting for the download to finish.

There are computers with several separate physical processors that really allow simultaneously execute several processes, but this, in addition to occupying more physical space on the motherboard, complicates a lot the circuitry, since it has to be duplicated for each processor. Tendency is to encapsulate several processors, called cores, inside the same microchip. This has the advantage of not requiring additional circuitry and allows a much faster communication between the different cores, as they are at a much reduced distance from each other.

In order to show the use of the different mechanisms that allow to implement multitasking, I have prepared an application that will contain the code examples used in the series. In this link you can download the MultithreadingDemo application source code, written in csharp with Visual Studio 2015. The application uses .NET Framework 4.5.

At the moment, there is only one option, in the Demo menu, which lets you compare the performance of the different classes provided by the platform. It is the Tasks option, which opens an MDI window that looks like this:

Tosks option in MultithreadingDemo
Tasks option in MultithreadingDemo

On the right side you can see the four buttons that allow to execute the same process using different mechanisms and, below them, you can select the number of threads or processes to use in some of the options.

The process that I will use as an example is the drawing of a part of a fractal set, which requires a lot of calculations using variables of type double. The set is drawn in a region of 640 x 480 pixels, which is stored in the _bitmap global variable, an array of int.

With the Single button, the process is launched sequentially, without using multitasking at all. The calculation is done 36 times to perform an average of the run time, discarding the first 6 executions, which tend to be slower than the later ones. This is the loop in which the calculations are performed:

for (int i = 0; i < 36; i++)
{
double pi, qi;
double x, y, xx, yy, px, py;
pi = qi = _cCte;
px = _cX;
py = _cY;
DateTime dt = DateTime.Now;
for (int p = 0; p < 640; p++)
{
for (int q = 0; q < 480; q++)
{
xx = -0.01 + p * pi;
yy = 0.02 + q * qi;
for (int k = 1; k <= 1000; k++)
{
x = xx * xx - yy * yy + px;
y = 2 * xx * yy + py;
xx = x;
yy = y;
if (x * x + y * y > _cR)
{
_bitmap[p + 640 * q] = _colors[k % 10];
break;
}
}
}
}
if (i > 5)
{
sum += DateTime.Now.Subtract(dt).TotalMilliseconds;
}
}

When finished, the process time in milliseconds appears at the right of the button, and a Bitmap is created in which the results of the calculation are copied, which is displayed in a PictureBox control:

bmp = new Bitmap(640, 480);
bf = bmp.LockBits(new Rectangle(0, 0, 640, 480),
ImageLockMode.ReadWrite, PixelFormat.Format32bppRgb);
Marshal.Copy(_bitmap, 0, bf.Scan0, 640 * 480);
bmp.UnlockBits(bf);
bf = null;
pbDrawing.Image = bmp;

Single process result in MultithreadingDemo
Single process result in MultithreadingDemo

It is better to run the application from outside the Visual Studio IDE and compiled with the Release configuration in order to get the most realistic times.

The first and oldest option to run this same process using multiple threads is to use the Thread class, in the System.Threading namespace. When you create an object of this type, you have to assign a method that will contain the code that the task should execute. In this case, this is the SectorThread method, which draws only one sector of the final set. The sector parameter is an integer that indicates which part of the set must draw the task, which depends on the number of processes selected. For example, with two processes, two tasks will be launched, each of which will draw half of the set. Here is the code to execute:

private void SectorThread(object sector)
{
double pi, qi;
double x, y, xx, yy, px, py;
pi = qi = _cCte;
px = _cX;
py = _cY;
for (int p = _xIndex((int)sector, true);
p < _xIndex((int)sector, false); p++)
{
for (int q = _yIndex((int)sector, true);
q < _yIndex((int)sector, false); q++)
{
xx = -0.01 + p * pi;
yy = 0.02 + q * qi;
for (int k = 1; k <= 1000; k++)
{
x = xx * xx - yy * yy + px;
y = 2 * xx * yy + py;
xx = x;
yy = y;
if (x * x + y * y > _cR)
{
_bitmap[p + 640 * q] = _colors[k % 10];
break;
}
}
}
}
}

_xIndex and _yIndex are two objects of type IndexDelegate that provide the initial and final limits of the sector corresponding to the current task, and depends on the number of processes involved.

private delegate int IndexDelegate(int sector, bool initial);
private IndexDelegate _xIndex = null;
private IndexDelegate _yIndex = null;
private int _processes = 4;

In general, Thread objects are created with a constructor that receives as a parameter a ThreadStart object that indicates the method, without parameters, that the task must execute:

Thread t = new Thread(new ThreadStart(method));

As in this case the method needs a parameter that indicates the sector to draw, we must use another version of the constructor that allows indicate a method with a single parameter of type object, using in the constructor a parameter of type ParametrizedThreadStart:

Thread t = new Thread(new ParameterizedThreadStart(SectorThread));

Pressing the Thread button executes the code that creates as many Thread objects as indicated and stores them in a list. Then each task is launched using the Start method, with the index of the sector to be drawn passed as a parameter. The use of the local variable sc is necessary because, if we use the loop index directly, the task can be started when the value of this variable has already been changed to the next value, using an incorrect index.

for (int i = 0; i < 36; i++)
{
threads.Clear();
for (int it = 0; it < _processes; it++)
{
threads.Add(new Thread(new ParameterizedThreadStart(SectorThread)));
}
DateTime dt = DateTime.Now;
for (int it = 0; it < threads.Count; it++)
{
int sc = it;
threads[it].Start(sc);
}
bool alive = true;
while (alive)
{
alive = false;
foreach (Thread t in threads)
{
alive = alive || t.IsAlive;
if (alive)
{
break;
}
}
}
if (i > 5)
{
sum += DateTime.Now.Subtract(dt).TotalMilliseconds;
}
}

Once all the tasks are launched, we wait for all of them to be finished in a while loop, inspecting the IsAlive property of each one. As in the previous case, we perform an average of 30 executions, discarding the first 6. In a system with several processors or cores we can observe that the execution takes considerably less time than in the case of using a single process.

In version 4 of .NET Framework, an enhanced version of the Thread class, the Task class, in the System.Threading.Tasks namespace, was added. This class makes optimal use of the different processor cores, as well as providing a much richer interface for handling tasks. In version 4 of the .NET Framework, the tasks are created and launched at the same time by calling the StartNew method of the static property Factory of the Task class, as follows:

Task t = Task.Factory.StartNew(() => { SectorThread(s); });

As you can see, the code to be executed by the task is directly provided by an expression. In .NET Framework version 4.5 you can do the same using the Run static method of the Task class:

Task t = Task.Run(() => { SectorThread(s); });

Using the Task button of the application we can check the performance of this option. Depending on the operating system and the number of processors this value may be higher, lower or almost equal to that of the Thread class, but in general it is preferable to use the Task class because it provides much richer control mechanisms than the Thread class. This is the code that launches the different tasks using the Task class, the procedure is the same as in the previous case, storing the tasks in a list:

for (int i = 0; i < 36; i++)
{
foreach (Task t in tasks)
{
t.Dispose();
}
tasks.Clear();
DateTime dt = DateTime.Now;
for (int ix = 0; ix < _processes; ix++)
{
int s = ix;
Task t = Task.Run(() => { SectorThread(s); });
tasks.Add(t);
}
Task.WaitAll(tasks.ToArray());
if (i > 5)
{
sum += DateTime.Now.Subtract(dt).TotalMilliseconds;
}
}

Here we can wait for all tasks to finish using the static WaitAll method of the Task class.

The last class we are going to review is Parallel, in the System.Threading.Tasks namespace. This class lets you use the code inside a for or foreach loop using a different task for each iteration. To do this, it has the static methods For and ForEach. This is the class that makes the most optimal use of the different cores of the processor, although it does not guarantee that the different indexes of the loop will be executed in order, so it is not valid in cases in which the order is important.

Using the Parallel button we will launch the following code:

for (int i = 0; i < 36; i++)
{
DateTime dt = DateTime.Now;
Parallel.For(0, 640, p =>
{
for (int q = 0; q < 480; q++)
{
double x, y, xx, yy;
xx = -0.01 + p * _cCte;
yy = 0.02 + q * _cCte;
for (int k = 1; k <= 1000; k++)
{
x = xx * xx - yy * yy + _cX;
y = 2 * xx * yy + _cY;
xx = x;
yy = y;
if (x * x + y * y > _cR)
{
_bitmap[p + 640 * q] = _colors[k % 10];
break;
}
}
}
});
if (i > 5)
{
sum += DateTime.Now.Subtract(dt).TotalMilliseconds;
}
}

As you can see, we use the Parallel.For loop for each of the vertical columns of the set. The first two parameters are the initial and final indexes of the loop, and the third is an expression with the indexed variable and the code fragment to be executed. It is important to define the local variables within the for loop, since each iteration is going to execute in a different process. This is the result of executing the four options:

Final result in MultitaskingDemo
Final result in MultithreadingDemo

Of course, these times will vary every time you execute each of the options, and not always the most optimal procedures will run faster than the rest, but in general it is preferable to use Parrallel, if possible, to use Task, and use Task instead of Thread. Of course, multithreading, if there are multiple processors, will always be preferable to using a single process.

That's all for now. In the next article I will show you how to synchronize some tasks between them.

Share this article: Share in Twitter Share in Facebook Share in Google Plus Share in LinkedIn
Comments (0):
* (Your comment will be published after revision)

E-Mail


Name


Web


Message


CAPTCHA
Change the CAPTCHA codeSpeak the CAPTCHA code