Parallel Series: Video – 02 PLINQ

claqueta

En este segundo vídeo de las Parallel Series haremos un breve recorrido por las principales características de Parallel LINQ.

Con la llegada de la Task Parallel Library, se abre un mundo de posibilidades gracias a PLINQ, que permite -de forma extremadamente sencilla- convertir cualquier consulta LINQ secuencial en una consulta paralelizable, permitiendo su segmentación y ejecución en los distintos cores en paralelo.

Como todos los videos de esta serie se trata de un vídeo de corta duración (mi intención es que todos duren 15 minutos aproximadamente), para que pueda ser visto en cualquier momento y además facilite la portabilidad a dispositivos móviles. Hoy en día hay que aprovechar cualquier momento para formarse! :-)

Enlaces relacionados:

Parallel Series: Parallel LINQ (PLINQ)

Parallel Series: Video – 01 Bases

Volver al índice de contenidos

Parallel Series: La clase estática Parallel

3 métodos para los reyes elfos bajo el cielo

Hoy quiero hablaros de la clase estática Parallel. Esta clase provee soporte para paralelizar bucles y regiones, y al igual que PLINQ su uso es muy sencillo. Cabe destacar que está especialmente optimizada para iteraciones, y que en este contexto se desenvuelve un poco mejor que PLINQ. No hay una diferencia significativa en tiempos absolutos, pero puede verse perfectamente si utilizamos el magnífico profiler de Visual Studio 2010. No obstante, pueden existir situaciones en las que si se necesita afinar mucho el rendimiento en iteraciones, y aquí es dónde tiene más sentido utilizar dos de los tres métodos de esta clase: For y ForEach. Al tercero lo llamaremos Cirdan y apenas aparecerá en esta historia (en realidad me refiero a Invoke pero tampoco aparecerá por aquí).

Parallel_Class

Comprendiendo las acciones

Los dos métodos tienen una firma muy similar en su forma más sencilla. Ambos iteran sobre una serie de instrucciones realizando n veces cada una de ellas. Y aquí es dónde vemos aparecer los parámetros de tipo Action:

public static ParallelLoopResult For
    (int fromInclusive, int toExclusive, Action<int> body)
public static ParallelLoopResult ForEach<TSource>
    (IEnumerable<TSource> source, Action<TSource> body)

Un Action<T>, al igual que su hermano Func<T> es uno de los elementos de C# importados de la programación funcional, y desde el momento en que uno se acostumbra a usarlo, cuesta pensar cómo ha podido desarrollar toda su vida anterior. Si no, los que estéis acostumbrados a usar expresiones lambda en LINQ, imagináos que desaparecen de un día para otro.

No quiero empezar a divagar ahora sobre programación funcional, aunque si que quiero hacer incapié en el uso de Actions y lo importantes que se han vuelto en los últimos años. De hecho, recientemente he dedicado un post a cómo Action y Func han simplificado mucho el trabajo con delegados a los desarrolladores.

El método Parallel.For

Pero volviendo al tema que nos ocupa, si observamos la firma del método Parallel.For podremos ver que en lo importante no difiere demasiado de su homólogo for de toda la vida: Ambos tienen un inicio, un final y unas acciones a realizar un número determinado de veces.

Así que partiendo del método IsPrime que ya utilizamos en el anterior post sobre PLINQ, vamos a ver una comparativa entre las sintaxis de éstos dos métodos:

for (int i = 0; i < 100; i++)
{
    if(i.IsPrime())
        Console.WriteLine(string.Format("{0} es primo", i));
    else
        Console.WriteLine(string.Format("{0} no es primo", i));
}

Parallel.For(0, 100, (i) =>
    {
        if (i.IsPrime())
            Console.WriteLine(string.Format("{0} es primo", i));
        else
            Console.WriteLine(string.Format("{0} no es primo", i));
    });

En ambos  casos tenemos una serie de líneas que deben ejecutarse 100 veces. Concretamente desde 0 hasta 99, ya que el elemento superior no se incluye en ninguno de los dos casos. Sólo se ve un poco extraño el uso del Action<T>, pero podéis pensar en que la variable int i del primer bucle for, aquí se transforma en la parte (i) a la izquierda de la expresión lambda (=>). Y las acciones a ejecutar del primer for son exactamente iguales y van a la derecha de la expresión lambda.

So, let’s parallelize!

Viéndolo de este modo debe resultar extremadamente sencillo transformar todos nuestros bucles de este modo, así que ¿debemos hacerlo? La respuesta es NO.

En algunas ocasiones no vamos a obtener rendimiento por el hecho de paralelizar, ya que si el trabajo a realizar es mínimo, tardaremos más tiempo en dividir el trabajo en distintos threads, ejecutarlos y consolidar la información que en ejecutar la tarea sin paralelizar. También podría ser que nos encontrásemos un cuello de botella externo en un dispositivo de I/O, como un puerto, un servidor remoto o un socket.

Otro claro ejemplo de esto son los bucles anidados. Es común anidar varias estructuras for o foreach para realizar ciertos algoritmos. En este caso el candidato a ser paralelizado siempre es el bucle exterior y no es necesario (de hecho sería contraproducente) paralelizar los bucles internos:

Parallel.For(0, 100, (z) =>
    {
        for (int i = 0; i < 100; i++)
        {
            if (i.IsPrime())
                Console.WriteLine(string.Format("{0} es primo", i));
            else
                Console.WriteLine(string.Format("{0} no es primo", i));
        }
    });

Por lo pronto resulta bastante evidente, ya que si paralelizamos en bucle exterior necesitaríamos un ordenador con 100 cores y evidentementemente todavía no existen, así que la TPL tiene que agrupar estas tareas para adaptarlas a los cores disponibles, tardando cierto tiempo en hacer la sincronización (parecido a los primeros ejemplos con monos de la serie). Imagináos entonces si paralelizamos ambos bucles: 100 x 100 = 10.000 cores? Simplemente no tiene sentido.

Mi consejo es que en todos los casos en los que se decida paralelizar un bucle (y esto también vale para las consultas PLINQ) se realice primero una comparativa de rendimiento.

ParallelFor_Profiling

El método Parallel.ForEach

En cuanto al método ForEach es prácticamente igual al anterior con la salvedad que no tenemos un inicio y un final, sino una secuencia de entrada de datos (basada en IEnumerable, como PLINQ) y una variable que usamos para iterar por cada uno de los elementos de la secuencia y realizar una serie de acciones.

Consideremos el siguiente código:

List<FeedDefinition> feeds = new List<FeedDefinition>();
clock.Restart();
var blogs = FeedsEngine.GetBlogsUrls();
foreach (var blog in blogs)
{
    feeds.AddRange(FeedsEngine.GetBlogFeeds(blog));
}
clock.Stop();
this.Text = clock.ElapsedMilliseconds.ToString("n2");
feeds.ForEach(p => Console.WriteLine(p.Name));

Suponiendo que tenemos un método FeedsEngine.GetBlogsUrls que devuelve una lista de urls de proporcionan contenido RSS, el código anterior se conecta a cada una de las urls e intenta descargar toda la información de los posts mediante un método FeedsEngine.GetBlogFeeds(blog).

Nota: El código completo lo podréis encontrar en el post (todavía no publicado) ‘Código de ejemplo de las Parallel Series’, que contiene todos los ejemplos de todos los posts de la serie.

Como podéis imaginar este proceso totalmente secuencial es un serio candidato a ser paralelizado, ya que la mayoría del tiempo de este proceso es tiempo desperdiciado intentando a conectar con un servidor externo y que éste responda a las peticiones. En este caso paralelizar va a ser de gran ayuda aunque es importante comprender que en este caso la ganancia de rendimiento no va a ser por usar más potencia local, sino por lanzar las peticiones a los distintos servidores de forma asíncrona.

Así pues, basta cambiar la parte del bucle foreach por su versión paralelizada:

Parallel.ForEach(blogs, (blog) =>
    {
        feeds.AddRange(FeedsEngine.GetBlogFeeds(blog));
    });

En la que definimos la secuencia de datos a utilizar y declaramos la variable blog al vuelo (el compilador infiere el tipo automáticamente) a la izquierda de la expresión lambda, y a la derecha las acciones que deseamos realizar, que son exactamente iguales a la anterior versión foreach.

Y comprobaremos como se ejecuta mucho más rápido. En mi estación de trabajo pasamos de 6,7 segundos a 1,4 lo que no está nada mal.

Explorando más opciones

En la clase Parallel al igual que en las consultas PLINQ, existe la posibilidad de especificar el grado de paralelismo así como de cancelar la ejecución de un bucle. Sólo debemos usar una de las sobrecargas que utiliza un objeto de tipo ParallelOptions.

private void button11_Click(object sender, EventArgs e)
{
    CancellationTokenSource cs = new CancellationTokenSource();
    var cores = Environment.ProcessorCount;
    clock.Restart();
    var options = new ParallelOptions() {
        MaxDegreeOfParallelism = cores / 2,
        CancellationToken= cs.Token };
    try
    {
        Parallel.For(1, 10, options,
            (i) =>
            {
                dowork_cancel(i, cs);
            });
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
    clock.Stop();
    this.Text = clock.ElapsedMilliseconds.ToString("n2");
}

void dowork_cancel(int i, CancellationTokenSource cs)
{
    Thread.Sleep(1000);
    if (i == 5) cs.Cancel();
}

En el caso anterior especificamos un grado de paralelización de la mitad del número de cores y preparemos la consulta para su posible cancelación (algo que simulamos en el interior del método dowork_cancel al llegar el contador a 5).

Próximanante en sus pantallas…

Todavía no he terminado ni la mitad de los posts de esta serie y ya estoy viendo que sería muy interesante ampliarla, mostrando ciertas características avanzadas que -por extensión- no quiero incluir en estos posts, más introductorios.

Más adelante veremos cómo salir o parar (no es lo mismo) un bucle parallelizado mediante un objeto ParallelLoopState, lidiar con variables compartidas o inicializar variables locales a cada partición. Pero eso lo dejamos para los posts avanzados de las Parallel Series.

Volver al índice de contenidos

Windows 8: Microsoft’s full ARM tablet demo

Microsoft introduced a number of tablet concepts during a partner preview event earlier this week.

win8tabletarmpreview

The software giant held a technical demonstration at Computex on Thursday to demonstrate Windows 8 to hardware partners. Mike Angiulo, corporate vice president of Windows Planning, Hardware and PC Ecosystem at Microsoft, demonstrated Windows 8 on a variety of tablets. Angiulo showed how Windows 8 works across x86 and ARM-based chips. Microsoft previously introduced a technology preview of its Windows ARM support by showing off an early build of Windows 8 at CES 2011. Microsoft is partnering with ARM-based manufactures NVIDIA, Qualcomm and Texas Instruments to produce new Tablet devices.

See the full article and video al Winrumors:

http://www.winrumors.com/windows-8-watch-microsofts-full-arm-tablet-demo-video/

Edit: My friend Hadi just tweet about more videos on Windows8:

Hadi Hariri

More Windows 8 Videos: http://hadih.me/jXI0ip

Windows 8 first ‘official’ preview

REDMOND, Wash. – June 1, 2011 – Today, at the D9 Conference, we demonstrated the next generation of Windows, internally code-named “Windows 8,” for the first time. Windows 8 is a reimagining of Windows, from the chip to the interface. A Windows 8-based PC is really a new kind of device, one that scales from touch-only small screens through to large screens, with or without a keyboard and mouse.

See the full article at:

http://www.microsoft.com/presspass/features/2011/jun11/06-01corporatenews.aspx

Edit: Microsoft made it clear last night that — contrary to expectations and rumors — the new shell is going to be the default on all Windows 8 PCs, and not just tablets. Many of us Microsoft watchers had believed it would be the default on tablets. Source: Mary Jo’s blog

Parallel Series: Parallel LINQ (PLINQ)

LINQ power!

Creo que estaremos todos de acuerdo en que LINQ ha supuesto una revolución en la forma de desarrollar, y ha hecho que muchos desarrolladores de otros lenguajes nos miren con cierto tono de envidia… E incluso que otras plataformas estén haciendo serios esfuerzos para incorporarlo en sus Frameworks :-)

Ahora, con la llegada de la Task Parallel Library, se abre un mundo de posibilidades gracias a PLINQ, que permite -de forma extremadamente sencilla- convertir cualquier consulta LINQ secuencial en una consulta paralelizable, permitiendo su segmentación y ejecución en los distintos cores en paralelo.

Es decir, cualquier consulta LINQ como ésta, en la que tenemos un array llamado numbers y un método IsPrime que devuelve un valor boolean en función de si un número es primo:

var query =
    from n in numbers
    where n.IsPrime()
    select n;

Puede ser paralelizada simplemente agregando esto:

var query =
    from n in numbers.AsParallel()
    where n.IsPrime()
    select n;

Método IsPrime:

public static class BaseTypesExtensions
{
    public static bool IsPrime(this int n) //1 = false, 2 = true, 3 = true...
    {
        if (n <= 1) return false;
        if ((n & 1) == 0)
        {
            if (n == 2) return true;
            else return false;
        }
        for (int i = 3; (i * i) <= n; i += 2)
        {
            if ((n % i) == 0) return false;
        }
        return n != 1;
    }
}

Partiendo de que el array de números contiene 10 millones de números enteros, y de que mi estación de trabajo actual tiene un procesador i7 con 8 cores, el resultado es abrumador:

La consulta LINQ tarda  5,2 segundos frente a los 1,3 segundos de la segunda.

Es decir, casi 4 segundos menos o un 400% más rápido.

¿Dónde está la magia?

La magia verdadera es que PLINQ nos abstrae de todo el proceso de paralelización, creación de tareas, threads, sincronización y consolidación de los datos.

Y además creo que lo hace de forma muy elegante :-)

Como ya sabemos, las consultas LINQ to objects se basan en IEnumerable<T> (gracias Generics!) que expone un enumerador para recorrer los elementos de una secuencia de elementos de tipo T. Esto hace que todas las colecciones que puedan devolverse en este tipo de consultas (IOrderedEnumerable, IQueryable, etc.) implementen esta interfaz. Hasta aquí nada nuevo bajo el sol.

Sin embargo, en la consulta PLINQ al utilizar el método extensor AsParallel() estamos transformando la secuencia de entrada de IEnumerable<T> a ParallelQuery <T> permitiendo la segmentación de los elementos de la secuencia y ejecutando cada uno de los segmentos en un thread distinto. Y por supuesto, repartiendo el trabajo en los diversos cores (si los hay).

AsParallel

La secuencia de entrada se particiona y se manda por fragmentos a distintos threads que invocan al método IsPrime devolviendo true (T) o false (F), y posteriormente los consolida en una secuencia de salida que puede ser consumida.

No obstante, el hecho de paralelizar el trabajo no garantiza que el resultado sea devuelto en el mismo orden, ya que es posible que un thread termine antes que otro y devuelva su resultado parcial antes de lo esperado. Así que, si la ordenación de los datos de salida es importante tenemos que ir un paso más allá.

asordered

Los primeros elementos deberían ser 2, 3, 5, 7… no 59 y 71 ¿?

PLINQ y la ordenación

Para asegurar la ordenación del conjunto de resultados, basta agregar el método AsOrdered() a la consulta. Este método asegura la correcta ordenación, a costa de implementar una serie de mecanismos de sincronización. Estos mecanismos, lógicamente retardan un poco el tiempo de entrega de los resultados, pero es despreciable. En mi estación de trabajo se arrojan unos valores de 1,311 segundos sin ordenar frente a 1,344 segundos ordenados (apenas 30 milésimas). Estos resultados son la media de una serie de 50 mediciones, con lo que son bastante fiables.

Una vez modificada la consulta:

var query =
    from n in numbers.AsParallel().AsOrdered()
    where n.IsPrime()
    select n;

El resultado es claro:

asordered2

Especificar el grado de paralelización

En la mayoría de las charlas que he dado sobre la TPL se acostumbra a preguntar respecto a funciones que acceden a recursos externos (servicios, sockets, etc.). En estos casos aparece claramente un cuello de botella, y no porque una función necesite hacer uso intensivo de la CPU, sino porque debe esperar un resultado externo. Aquí suele ser interesante especificar el grado de paralelización de deseamos. Otro caso interesante para especificar el grado de paralelización puede ser el típico escenario de productor/consumidor.

Es interesante notar que al especificar el grado de paralelización no estamos forzando a que se usen n particiones, sino que simplemente estamos especificando el valor máximo:

var cores = Environment.ProcessorCount;
var query =
    from n in numbers.AsParallel().AsOrdered().
        WithDegreeOfParallelism(cores / 2)
    where n.IsPrime()
    select n;

De este modo, al definir el grado de paralelización en la mitad del número de cores del procesador nos aseguramos que (por ejemplo) podremos tener un hilo que vaya creando elementos (productor) y otro hilo que vaya consumiendo dichos elementos (consumidor).

Cancelación de una consulta PLINQ

En ocasiones, una consulta PLINQ puede ser cancelada. Bien porque durante el proceso se ha encontrado un error y ya no es necesario terminar de calcular el resto de resultados, o simplemente porque ha sido cancelada por el usuario.

Es estos casos, es necesario utilizar un token de cancelación. Este token tiene su origen en la estructura CancellationTokenSource, que representa ‘una potencial cancelación’ y proporciona los mecanismos para cancelar y comprobar el estado de una tarea asíncrona, de modo que puede utilizarse con todos los elementos de la Task Parallel Library, no sólo con PLINQ.

A continuación, vamos a modificar el código del ejemplo que hemos usado hasta ahora para simular un error y comprobar el funcionamiento de la cancelación de tareas en PLINQ. Para ello lo primero que vamos a hacer es crear una sobrecarga del método IsPrime, que reciba un parámetro de tipo CancellationTokenSource, para poder cancelar la tarea:

public static bool IsPrime(this int n, CancellationTokenSource cs)
{
    if (n == 1000) cs.Cancel();
    return IsPrime(n);
}

A modo de ejemplo, cuando el número a calcular sea 1.000 cancelaremos la tarea, de modo que no sea necesario llegar a los 10 millones. De este modo, por un lado se lanzará una excepción y por otro el tiempo en ejecutar la consulta PLINQ será mucho menor.

private void plinq_cancellable()
{
    var numbers = Enumerable.Range(1, 10000000);
    using (var cs = new CancellationTokenSource())
    {
        clock.Restart();
        var query = numbers.AsParallel().AsOrdered().
            WithCancellation(cs.Token).
            Where(p => p.IsPrime(cs));
        try
        {
            var result = query.ToList();
        }
        catch (OperationCanceledException ex)
        {
            Console.WriteLine(ex.Message);
        }
        clock.Stop();
        this.Text = clock.ElapsedMilliseconds.ToString("n2");
    }
}

Por un lado tenemos que tener la precaución de envolver la consulta dentro de un bloque try-catch (en este caso sólo la llamada a ToArray() que es realmente cuando se ejecuta la consulta), y por el otro especificamos que la consulta puede ser cancelada mediante WithCancellation. A continuación creamos un objeto de tipo CancellationTokenSource para administrar la cancelación de esta consulta. Este objeto será el que finalmente pasemos al método IsPrime() y en caso que se cancele provocará que su propiedad IsCancellationRequested devuelva true y que se produzca una bonita excepción de tipo OperationCanceledException.

WithCancellation

Limitaciones de PLINQ

No quiero extenderme mucho más porque creo que hay material suficiente para hacer un post más adelante sobre temas avanzados. Sin embargo quiero dejar claro que existen algunas limitaciones en PLINQ, como el uso de algunos operadores (Take, SkipWhile) y de las versiones indexadas de Select o ElementAt.

Además existen otros casos en los que por cuestiones de rendimiento no es recomendable usar PLINQ en todos los casos, debido al sobrecoste que puede llegar a ocasionar, como el uso de Join, Union o GroupBy. Sin embargo, trataremos éstas cuestiones más adelante.

Próximamente veremos cómo utilizar la clase estática Parallel, optimizada para trabajar con procesos iterativos, esos típicos bucles que todas las aplicaciones tienen.

Volver al índice de contenidos

Using ASP.NET Membership in SQL Azure

The scenario:

Sometimes we forget SQL Azure is not SQL Server. This morning in the project I’m working, we needed to create a small ASP.NET membership database in SQL Azure.

It seems easy! Would you expect troubles with this? Nope? Ok, me neither… until I tried to execute aspnet_regsql to create the database:

An error occurred during the execution of the SQL file ‘InstallCommon.sql’. The SQL error number is 208 and the SqlException message is: Invalid object name ‘master.dbo.sysdatabases’.

Notice that this error is absolutely normal, because in SQL Azure we can’t access to sysdatabases table. In fact, if we try to execute this script:

SELECT * FROM master.dbo.sysdatabases

We will receive this error:

Reference to database and/or server name in ‘master.dbo.sysdatabases’ is not supported in this version of SQL Server.

The solution:

When using SQL Azure we cannot create the database with aspnet_regsql. Instead, we should use a new set of scripts optimized for SQL Azure.

There are a couple of files to download, but we only need InstallAspSchema.zip which contains all the needed scripts separated by themes (memberships, roles, etc.) which I think is great! So we will install only the parts that we will use:

aspnet_scripts

The most important step is installing the scripts in the correct order. The first one must be InstallCommon.sql, and after (in my particular case) installing the InstallMemberShip.sql and InstallRoles.sql scripts is -by far- enough.

Note: Be aware with the scripts, I’ve had to execute the membership script twice, because the first one some tables haven’t been created. If you have the same problem please send me feedback about it.

Hope you enjoy it! :-)

More info:

ASP.NET Membership scripts for SQL Azure:

http://archive.msdn.microsoft.com/KB2006191

General Guidelines and Limitations (SQL Azure Database)

http://msdn.microsoft.com/en-us/library/ee336245.aspx

Macbook Pro – Series 3400 series chipset usb problems?

After a long time… here we go again!

Today a quick post, just a note:

If you bought a MacBook Pro and you would like running Windows 7, it could be possible experiment some chipset problems. In my particular case with USB and bluetooth devices (Wireless Notebook Presenter 8000), and a couple of window-freezed.

Also, in some ocasions Windows threw an error: “Intel 5 Series/3400 Series Chipset Family USB Universal Host Controller – 3B3B (and 3B36) has a driver problem”.

After navigating 10 minutes I just realized that lots of people are experimenting the same trouble, and I would like to show how to solve it in 3 simple steps:

  1. Open your ‘Device Manager’
  2. Show all devices (including hide devices, View menu)
  3. Disable both devices (3B3B & 3B36)

That’s all! :-D

Hope you enjoy this. Leave a comment if this post has been helpfull ;-)

Parallel Series: Video – 01 Bases

claqueta

En este primer video de las Parallel Series veremos qué es la programación paralela y el cambio de paradigma que va a suponer en el desarrollo de software.

En la primera parte veremos un poco de historia y conoceremos los motivos que nos han llevado hasta aquí, mientras que en la segunda nos centraremos en aclarar algunos conceptos base, necesarios para el correcto desarrollo de aplicaciones que utilicen la Task Parallel Library.

Como todos los videos de esta serie se trata de un vídeo de corta duración (mi intención es que todos duren 15 minutos aproximadamente), para que pueda ser visto en cualquier momento y además facilite la portabilidad a dispositivos móviles. Hoy en día hay que aprovechar cualquier momento para formarse! :-)

Enlaces relacionados:

Un poco de historia

Aclarando conceptos base

Volver al índice de contenidos

Parallel Series: Aclarando conceptos base

Cuando hablamos de programación paralela conviene tener bastante claros algunos conceptos a nivel de sistema operativo. En este apartado trataremos de aclarar estos términos, ya que más adelante los usaremos frecuentemente.

Procesos (Processes)

Toda aplicación ejecutándose en el sistema operativo existe dentro del contexto de un proceso, aunque no todos los procesos se corresponden con aplicaciones visibles. Basta abrir el administrador de tareas para comprobar que la lista de procesos es bastante mayor a la de aplicaciones. Eso es así porque pueden corresponderse a servicios, aplicaciones no visibles o porque algunas aplicaciones -como algunos navegadores- están diseñadas para crear varios procesos.

Processes

Un proceso proporciona los recursos necesarios para ejecutar un programa. Contiene un espacio de memoria virtual, código ejecutable, un contexto de seguridad, un identificador de proceso único, variables de entorno, y al menos un thread de ejecución.

Process

Cada proceso se inicia con un único thread, a menudo llamado thread principal. Pero puede crear threads adicionales, que pueden ser utilizados para encargarse de diferentes tareas.

Hacer llamadas entre procesos resulta complejo y muy costoso en términos de rendimiento debido a que deben usarse mecanismos especiales como pipes, sockets o llamadas RPC (Remote procedure call).

Dominios de aplicación .NET (AppDomains)

Al ser .NET una plataforma que ejecuta código administrado, los procesos que se crean al ejecutar éstas aplicaciones son un poco diferentes, ya que cuando se diseñó .NET una de las máximas preocupaciones fue la de tratar de mejorar el manejo de los procesos clásico o no administrados. Por ello se creó el concepto de dominio de aplicación, que podría definirse como un proceso lógico dentro del proceso del sistema operativo.

AppDomain

La gran diferencia es que dentro de un proceso podemos crear distintos dominios de aplicación y cargar en cada uno de ellos varios ensamblados, y aprovechar que las llamadas entre distintos dominios de aplicación y los ensamblados que contienen son mucho más rápidas que entre procesos. Si uno de estos ensamblado debe ser compartido entre dos dominios de aplicaciones éste se copia en cada uno de los dominios.

De este modo, al usar los dominios de aplicación se obtiene la ventaja de aislar el código de un proceso a otro, pero sin pagar el sobrecoste dedicado a realizar llamadas entre procesos.

Hilos (Threads)

Un thread es la entidad dentro de un proceso encargada de ejecutar el código. Todos los threads que contiene un proceso comparten los recursos y memoria virtual de éste, y mantienen controladores de excepciones, una prioridad de programación, almacenamiento local, y un identificador de thread único.

Los threads son independientes a los dominios de aplicación, de forma que podemos pensar en ellos como elementos transversales que pueden saltar de un uno a otro a lo largo del tiempo. No existe ninguna correspondencia entre el número de threads y de dominios de aplicación.

Por defecto, todos los procesos se crean con un thread por defecto llamado thread principal, aunque en las aplicaciones .NET se crear al menos dos, ya que es necesario al menos un segundo thread para administrar el recolector de basura. No obstante cada proceso puede crear un número casi ilimitado de ellos, aunque en última instancia el sistema operativo siempre tiene la potestad de priorizar a la baja estos hilos o incluso congelarlos.

Thread

Realizar cambios de contexto entre threads es muchísimo más rápido que los cambios de contexto de proceso. De hecho en los sistemas operativos que utilizan multitarea preemptiva (la gran mayoría hoy en día) el sistema operativo va cediendo una pequeña fracción de tiempo a cada uno de los threads de cada uno de los procesos cargados para que ejecuten una porción de su código ejecutable, dando la sensación de que varias aplicaciones se ejecutan al mismo tiempo.

Este tipo de multitarea tiene la ventaja frente a sus predecesores de que si un proceso deja de responder, el sistema no se colapsa y puede seguir respondiendo sin verse afectado por la caída del mismo. Esto en la práctica ha significado la casi desaparición de las llamadas BSOD (Blue Screen Of Death).

Multihilo (Multithreading)

A la capacidad que tienen los procesos de crear distintos threads ejecutándose simultáneamente es a lo que llamamos Multithreading. Y nos ha permitido simular la multitarea en los ordenadores personales de la última década y media. Esto es así porque aunque físicamente sólo hay un microprocesador, en términos del sistema operativo éste cede un periodo de tiempo a cada thread de cada uno de los procesos cargados en el sistema, y al repetirse una y otra vez produce la sensación de que todas las aplicaciones se ejecutan al mismo tiempo, pero nada más lejos de la realidad. Al menos hasta hace poco.

Multithreading

Paralelismo (Parallelism)

Con la aparición de los primeros microprocesadores con varios núcleos, al fin se pudo ejecutar código en paralelo y obtener la tan deseada multitarea real, ya que distintos threads pueden ejecutarse en distintos cores al mismo tiempo. De modo que a más cores, más threads pueden ejecutarse y por consiguiente más código.

Parallelism

Hace apenas cuatro o cinco años de la aparición de los primeros dual core, sólo un poco más tarde aparecieron los quad core, y hoy en día es bastante habitual ver estaciones de trabajo con 8 y hasta 16 cores. En cuanto al futuro, nadie sabe al ritmo que evolucionará esta tecnología, pero los chicos de Intel hace más de dos años ya filtraron imágenes de un Windows Server 2008 con 256 cores. Incluso los súper ligeros procesadores para teléfonos y tablets basados en ARM están empezando a lanzar modelos de dos y cuatro cores.

image

Volviendo al ejemplo de los monos, resulta muy tentador pensar que si aprovechamos toda la potencia de los nuevos núcleos podemos obtener ganancias de rendimiento espectaculares y escribir los 200 tomos en el tiempo que escribimos uno de ellos.

Evidentemente esta afirmación es un poco exagerada, ya que siempre va a haber un arduo trabajo de sincronización entre los diferentes monos… perdón threads. Con todo, la ganancia es espectacular, llegando fácilmente a multiplicar x5 o x6 en un entorno con 8 cores. Algo nada despreciable en según qué procesos. De modo que viendo el número de cores hacia dónde nos movemos, a mi juicio se hace imperativo conocer -si no dominar- la TPL.

Antes de terminar, un consejo

Como se ha visto, sólo puede existir paralelismo real en una estación con varios cores. De otro modo el código funcionará sin errores pero solo utilizando el único core de la máquina. Así que un error bastante común entre los desarrolladores es utilizar máquinas virtuales para desarrollar, y olvidarnos que la mayoría no permiten especificar varios cores. De modo que es bastante probable que alguna vez nos encontremos refunfuñando porque un código bien escrito no obtiene ninguna ganancia cuando lo ejecutemos dentro de una maquina virtual.

A continuación…

En el próximo post veremos cómo extender LINQ con Paralel LINQ, y de este modo dotar a nuestras consultas de paralelismo sin apenas impacto en el código actual.

Volver al índice de contenidos