news40 parallel computing

69
Parallel Computing with .NET 4.0 Sacando partido al multi-core Lluís Franco - AndorraDotNet

Upload: fimarge

Post on 25-May-2015

1.127 views

Category:

Documents


5 download

TRANSCRIPT

Page 1: News40 Parallel Computing

Parallel Computing with .NET 4.0

Sacando partido al multi-core

Lluís Franco - AndorraDotNet

Page 2: News40 Parallel Computing

2

Agenda

1. Programación paralela ¿Queéloqueé?2. Un poco de historia y algunos conceptos.3. ¿Multithreading versus Parallel?4. Paralelismo en .NET 4.0 (PLINQ, Parallel, Task)5. Demos…6. Demos……7. Y si… más demos

Page 3: News40 Parallel Computing

3

Let’s play!

Parallel Computing with .NET 4.0

Page 4: News40 Parallel Computing

4

1 - ¿Y eso queéloqueé?¿Esto?

Page 5: News40 Parallel Computing

5

1 - ¿Y eso queéloqueé?¿Acaso esto?

Page 6: News40 Parallel Computing

6

1 - ¿Y eso queéloqueé?

Wikipedia dixit:

“La computación paralela es una técnica de programación en la que muchas instrucciones se ejecutan simultáneamente. Se basa en el principio de que los problemas grandes se pueden dividir en partes más pequeñas que pueden resolverse de forma concurrente”.

“Existen varios tipos de computación paralela: paralelismo a nivel de bit, paralelismo a nivel de instrucción, paralelismo de datos y paralelismo de tareas”.

“Durante muchos años, la computación paralela se ha aplicado en la computación de altas prestaciones, pero el interés en ella ha aumentado en los últimos años debido a las restricciones físicas que impiden el escalado en frecuencia”.

Page 7: News40 Parallel Computing

7

1 - ¿Y eso queéloqueé?

La Ley de Moore expresa que aproximadamente cada 18 meses (*) se duplica el número de transistores en un circuito integrado. Se trata de una ley empírica, formulada por el co-fundador de Intel, Gordon E. Moore el 19 de abril de 1965, cuyo cumplimiento se ha podido constatar hasta hoy.

Ley de Moore:

(*) 2 años (1975).

Page 8: News40 Parallel Computing

8

1 - ¿Y eso queéloqueé?

• La ley de Moore ha muerto:– Tecnología actual 32 nanómetros – Límite 18 nanómetros (cambios)– Se alcanzará aprox. en 2014

• Solución:– Más núcleos– Computación en paralelo– INTEL / AMD / NVIDIA (CUDA)

«La materia presenta efectos cuánticos que harían necesaria unatecnología diferente

para seguir realizando cálculos a ese nivel».

Stephen Hawking

Page 9: News40 Parallel Computing

9

2 – Un poco de historia

¿Alguien recuerda los CRAY?1976: Es uno de los supercomputadores más conocidos y exitosos de la historia, y de los más potentes en su época

Page 10: News40 Parallel Computing

10

2 – Un poco de historia

¿Y a Deep Blue? 1997: El primer ordenador en batir a un gran maestro (Garry Kasparov)

Page 11: News40 Parallel Computing

11

2 – Algunos conceptos

Procesos & Threads en un sistema operativo

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. Cada proceso se inicia con un único thread, a menudo llamado thread principal, pero puede crear threads adicionales.

Page 12: News40 Parallel Computing

12

2 – Algunos conceptos

Procesos & Threads en un sistema operativo

Un thread es la entidad dentro de un proceso que realmente ejecuta el código.Todos los threads de un proceso comparten los recursos y memoria virtual. Además, cada thread mantiene controladores de excepciones, una prioridad de programación, almacenamiento local, y un identificador de thread único.A más threads, más tiempo de CPU. Lógico, no?

Page 13: News40 Parallel Computing

13

3 - ¿Multithreading vs. Parallism?

• Tipos de Multitarea:– Cooperativa o No Preemptiva (Windows anteriores a

Win95/WinNT y MacOS): Cada proceso se ocupa de pasar el control al siguiente proceso. Un mal diseño de una aplicación y CATACRACK!

– Preferente o Preemptiva (Windows Win95 y superiores, el resto de S.O. modernos): El sistema operativo es el encargado de ceder un tiempo a cada thread de cada proceso (time slice). Un mal diseño de una aplicación no implica el derrumbe del S.O. ya que se ejecutan en modo user.

Page 14: News40 Parallel Computing

14

3 - ¿Multithreading vs. Parallism?

• Multithreading:– Técnica que consiste en manejar varios threads en una aplicación:

– Permite realizar tareas asíncronas al ‘mismo tiempo’ (aunque realmente no es así).

– No es trivial (más complejidad, depuración, acceso prohibido a la interfaz de usuario).

ThreadStart job = new ThreadStart(HacerAlgo); Thread thread = new Thread(job); thread.Start();

Page 15: News40 Parallel Computing

15

3 - ¿Multithreading vs. Parallism?

Escenario nº1

Escenario nº2

Page 16: News40 Parallel Computing

16

3 - ¿Multithreading vs. Parallism?

Una aplicación puede incrementar el número de threads para producir la sensación de que varios procesos se llevan a cabo al mismo tiempo.Pero sólo es paralelismo REAL si existen varios cores que los ejecuten en paralelo.

3 1 1 1

varios cores

Page 17: News40 Parallel Computing

17

3 - ¿Multithreading vs. Parallism?

• Conclusión: Se parece pero no es lo mismo• Podemos tener multithreading en una

estación con un solo core, pero sólo podemos tener paralelismo real en una estación multi-core.

• Error clásico: Como desarrolladores, utilizar paralelismo en una máquina virtual (con un solo core), y nos pensamos que estamos haciendo paralelismo ->NOR!

Page 18: News40 Parallel Computing

18

3 - ¿Multithreading vs. Parallism?

• Esto SI es paralelismo

Page 19: News40 Parallel Computing

19

3 - ¿Multithreading vs. Parallism?

• Y más vale que nos preparemos:

Page 20: News40 Parallel Computing

20

4 - Paralelismo en .NET 4.0

Microsoft Task Parallel Library (TPL)

Page 21: News40 Parallel Computing

21

4 - Paralelismo en .NET 4.0

LINQ - PLINQ

• Proporciona extensiones del lenguaje para poder utilizar paralelismo en nuestras consultas LINQ.

DATA - Parallel

• Situaciones en las que se realiza la misma operación al mismo tiempo sobre los elementos de un origen de colección o matriz.

TASKS - Task

• Una tarea representa una operación asíncrona, se asemeja a la creación de un Thread o un ThreadPool, pero a un nivel más alto de abstracción.

Concurrent

• Proporcionan mecanismos para compartir colecciones y elementos entre varios hilos de ejecución (Thread-safe).

Más sencillo Más complejo

Page 22: News40 Parallel Computing

22

LINQ

4 - Paralelismo en .NET 4.0

PLINQ

Devuelve colecciones IEnumerable<T> / IQueryable<T>

Devuelve colecciones ParallelQuery<T>

Page 23: News40 Parallel Computing

23

PLINQ

4 - Paralelismo en .NET 4.0

AsParallel();AsOrdered();ForAll(action);WithDegreeOfParallelism(ncores);WithCancellation(CancellationToken);WithMergeOptions(ParallelMergeOptions);WithExecutionMode(ParallelExecutionMode);

ParallelQuery

Page 24: News40 Parallel Computing

24

Demo 1 {PLINQ}

• Partimos de una sencilla consulta LINQ, que calcula los números primos hasta 10.000.000

• El objetivo es transformar esta consulta en una consulta de ejecución paralela, mediante el uso de PLINQ.

• Posteriormente veremos la diferencia de tiempo de ejecución entre ambas.

Page 25: News40 Parallel Computing

25

Demo 1 {PLINQ}        public static bool IsPrime(int candidate)        { if (candidate == 1) return true;            if ((candidate & 1) == 0)            {                if (candidate == 2) return true;                else return false;            }            for (int i = 3; (i * i) <= candidate; i += 2)            {                if ((candidate % i) == 0)                    return false;            }            return candidate != 1;        }

Page 26: News40 Parallel Computing

26

        private void getPrimesLINQ()        {            int[] array = Enumerable.Range(1, 10000000).ToArray();            clock.Restart();            int[] primesLINQ =                 (from x in array  where IsPrime(x) select x).ToArray();            var list = primesLINQ.Take(100).ToList();            showValues(list);            clock.Stop();            elapsedTimeLabel.Text = clock.

ElapsedMilliseconds.ToString("n");        }

Demo 1 {PLINQ}

Page 27: News40 Parallel Computing

27

        private void getPrimesPLINQ()        {            int[] array = Enumerable.Range(1, 10000000).ToArray();            clock.Restart();            int[] primesLINQ =                 (from x in array.AsParallel() where IsPrime(x) select x).ToArray();            var list = primesLINQ.Take(100).ToList();            showValues(list);            clock.Stop();            elapsedTimeLabel.Text = clock.

ElapsedMilliseconds.ToString("n");        }

Demo 1 {PLINQ}

Page 28: News40 Parallel Computing

28

        private void getPrimesPLINQ()        {            int[] array = Enumerable.Range(1, 10000000).ToArray();            clock.Restart();            int[] primesLINQ =                 (from x in array.AsParallel().AsOrdered() where IsPrime(x) select x).ToArray();            var list = primesLINQ.Take(100).ToList();            showValues(list);            clock.Stop();            elapsedTimeLabel.Text = clock.

ElapsedMilliseconds.ToString("n");        }

Demo 1 {PLINQ}

Page 29: News40 Parallel Computing

29

        private void getPrimesPLINQ()        {            int[] array = Enumerable.Range(1, 10000000).ToArray();            clock.Restart();            int[] primesLINQ =                 (from x in array.AsParallel().AsOrdered()

.WithDegreeOfParallelism(numcores)) where IsPrime(x) select x).ToArray();            var list = primesLINQ.Take(100).ToList();            showValues(list);            clock.Stop();            elapsedTimeLabel.Text = clock.

ElapsedMilliseconds.ToString("n");        }

Demo 1 {PLINQ}

Page 30: News40 Parallel Computing

30

Demo 1 online{PLINQ}

Page 31: News40 Parallel Computing

31

Demo 2 {PLINQ}

• Recorrer el árbol de directorios del sitio y mostrar información de cada fichero.

• Uso de ForAll para aplicar una acción a todos los elementos devueltos por un objeto de tipo «ParallelQuery».

• Usado para sustituir a For / ForEach

ParallelQuery<T>.ForAll<T>(Action<T> action);

Page 32: News40 Parallel Computing

32

Demo 2 {PLINQ}

        private void printInfo(string filename)        {            FileInfo f = new FileInfo(filename);            byte[] bytes = File.ReadAllBytes(f.FullName);            FileSecurity fs = f.GetAccessControl();            IdentityReference identity =  fs.GetOwner(typeof(NTAccount));            IdentityReference group = fs.GetGroup(typeof(NTAccount));            string s = string.Format( "file '{0}' ({1} kb), owner '{2}' ({3}), accesed at '{4}'",                f.Name, (f.Length / 1024).ToString("n2"),  identity.Value, group.Value, f.LastAccessTime);            filesinfoListBox.Items.Add(s);        }

Page 33: News40 Parallel Computing

33

Demo 2 {PLINQ}

        private void getFilesForEach()        {            filesinfoListBox.Items.Clear();            clock.Restart();            var files = Directory.EnumerateFiles(                Server.MapPath(""), "*.*",                SearchOption.AllDirectories);            foreach (string s in files)            {                printInfo(s);            }            clock.Stop();            elapsedTimeLabel2.Text = clock.

ElapsedMilliseconds.ToString("n2");        }

Page 34: News40 Parallel Computing

34

Demo 2 {PLINQ}

        private void getFilesParallelForAll()        {            filesinfoListBox.Items.Clear();            clock.Restart();            var files = Directory.EnumerateFiles(                Server.MapPath(""), "*.*",                SearchOption.AllDirectories).AsParallel();            files.ForAll<string>(p => printInfo(p));            clock.Stop();            elapsedTimeLabel2.Text = clock.

ElapsedMilliseconds.ToString("n2");        }

Page 35: News40 Parallel Computing

35

Demo 2 online{PLINQ}

Page 36: News40 Parallel Computing

36

4 - Paralelismo en .NET 4.0

Parallel.ForParallel.ForEachParallel.Invoke

Parallel

Ejemplo en la demo nº5

Extensiones para el trabajo secuencial sobre datos.

Page 37: News40 Parallel Computing

37

Demo 3 {Parallel}

• Calcular los n primeros números de la serie de Fibbonacci.

• Veremos como usar la versión paralela del clásico bucle For (Parallel.For)

• Compararemos la diferencia de tiempo entre ambas (For vs. Parallel.For).

Page 38: News40 Parallel Computing

38

Demo 3 {Parallel}

        static int Fibonacci(int x)        {            if (x <= 1) return 1;            return 

Fibonacci(x - 1) + Fibonacci(x - 2);

        }

Page 39: News40 Parallel Computing

39

Demo 3 {Parallel}

        private void calculateFibonacciFor()        {            clock.Restart();            fibonacciListBox.Items.Clear();            for (int i = 1; i <= 40; i++)            {                fibonacciListBox.Items.Add(

Fibonacci(i).ToString());            }            clock.Stop();            elapsedTimeLabel.Text = clock.

ElapsedMilliseconds.ToString("n2");        }

Page 40: News40 Parallel Computing

40

Demo 3 {Parallel}

        private void calculateFibonacciParallelFor()        {            clock.Restart();            fibonacciListBox.Items.Clear();            Parallel.For(1, 40, i =>                {                    fibonacciListBox.Items.Add(

Fibonacci(i).ToString());                });            clock.Stop();            elapsedTimeLabel.Text = clock.

ElapsedMilliseconds.ToString("n2");        }

Page 41: News40 Parallel Computing

41

Demo 3 online{Parallel}

Page 42: News40 Parallel Computing

42

Demo 4 {Parallel}

• Crear un pequeño cliente de RSS para obtener los feeds de algunos blogs mediante LINQ2XML.

• Veremos como usar la versión paralela del clásico bucle ForEach (Parallel.ForEach)

• Compararemos la diferencia de tiempo entre ambas (ForEach vs. Parallel.ForEach ).

Page 43: News40 Parallel Computing

43

Demo 4 {Parallel}

    public class FeedDefinition    {        public string Name { get; set; }        public string Url { get; set; }        public DateTime Date { get; set; }        public int NumComments { get; set; }    }

Clase para almacenar información de cada uno de los posts:

Page 44: News40 Parallel Computing

44

Demo 4 {Parallel}

        List<string> urls = new List<string>();        private void addUrls()        {            urls.Clear();            urls.Add("http://weblogs.asp.net/scottgu/rss.aspx");            urls.Add("http://andorradotnet.com/blogs/MainFeed.aspx");            urls.Add("http://geeks.ms/blogs/MainFeed.aspx");            urls.Add("http://feeds2.feedburner.com/CienciaKanija");        }

Page 45: News40 Parallel Computing

45

Demo 4 {Parallel}        public List<FeedDefinition> loadFeeds(string path)        {            try            {                XDocument feedxml = XDocument.Load(path);                var feeds = from feed in feedxml.Descendants("item")                            select new FeedDefinition                            {                                Name = feed.Element("title").Value,                                Url = feed.Element("link").Value,                                Date = DateTime.Parse(

feed.Element("pubDate").Value),                                NumComments = int.Parse(

feed.Element(slashNamespace +  "comments").Value)                            };                return feeds.ToList();            }            catch (Exception)                            return null;                    }    }

Page 46: News40 Parallel Computing

46

Demo 4 {Parallel}

        private void getFeedsForEach()        {            clock.Restart();            addUrls();            List<FeedDefinition> allfeeds = 

new List<FeedDefinition>();            foreach (var url in urls)            {                allfeeds.AddRange(loadFeeds(url));            }            showFeeds(allfeeds);            clock.Stop();            elapsedTimeLabel2.Text = clock.

ElapsedMilliseconds.ToString("n2");        }

Page 47: News40 Parallel Computing

47

Demo 4 {Parallel}

        private void getFeedsParallelForEach()        {            clock.Restart();            addUrls();            List<FeedDefinition> allfeeds = 

new List<FeedDefinition>();            Parallel.ForEach(urls, url =>            {                allfeeds.AddRange(loadFeeds(url));            });            showFeeds(allfeeds);            clock.Stop();            elapsedTimeLabel2.Text = clock.

ElapsedMilliseconds.ToString("n2");        }

Page 48: News40 Parallel Computing

48

Demo 4 online{Parallel}

Page 49: News40 Parallel Computing

49

Demo 5 {TASK class}

• Ejecutar tareas implícitamente (Invoke):        public void InvokeSample()        {            Parallel.Invoke(() => {                Console.WriteLine("Begin first task...");                GetLongestWord(words);            },  // close first Action            () => {                Console.WriteLine("Begin second task...");                GetMostCommonWords(words);            }, //close second Action            () => {                Console.WriteLine("Begin third task...");                GetCountForWord(words, "species");            } //close third Action        ); //close parallel.invoke    }

Page 50: News40 Parallel Computing

50

Demo 5 {TASK class}

• Ejecutar tareas explícitamente (Task class):

            Task t1 = Task.Factory.StartNew(() => DoSomething());

            Task t1 = new Task(() => DoSomething()); t1.Start();

var t1 = Task.Factory.StartNew<int>(() => DoSomething()); Task.WaitAny(t1); int i = t1.Result;

Page 51: News40 Parallel Computing

51

Demo 5 {TASK class}

• Encadenar tareas (ContinueWith):        static void SimpleContinuation()        {            string path = @"C:\users\public\TPLTestFolder\";            try            {                var firstTask = new Task(() => CopyDataIntoTempFolder(path));                var secondTask = firstTask.ContinueWith((t) => CreateSummaryFile(path));                firstTask.Start();            }            catch (AggregateException e)            {                Console.WriteLine(e.Message);            }        }

Page 52: News40 Parallel Computing

52

Demo 5 {Task}

• Usar la clase Task para ejecutar varias tareas en paralelo.

• Aplicar filtros a imágenes (1024x768 ~ 1Mb)• El código de los filtros usa ‘unsafe’ para

ejecutar código con punteros¿Alguien de VB en la sala ?

Page 53: News40 Parallel Computing

53

Demo 5 {TASK class}

        private void ApplyFiltersSequentially()        {            clock.Restart();            ApplyFilterImage1();            ApplyFilterImage2();            ApplyFilterImage3();            ApplyFilterImage4();            ApplyFilterImage5();            ApplyFilterImage6();            clock.Stop();            elapsedTimeLabel.Text = clock.ElapsedMilliseconds.ToString("n");        }

Page 54: News40 Parallel Computing

54

        private void ApplyFiltersParallel()        {            clock.Restart();            Task t1 = Task.Factory.StartNew(() => ApplyFilterImage1());            Task t2 = Task.Factory.StartNew(() => ApplyFilterImage2());            Task t3 = Task.Factory.StartNew(() => ApplyFilterImage3());            Task t4 = Task.Factory.StartNew(() => ApplyFilterImage4());            Task t5 = Task.Factory.StartNew(() => ApplyFilterImage5());            Task t6 = Task.Factory.StartNew(() => ApplyFilterImage6());            Task.WaitAll(t1, t2, t3, t4, t5, t6);            clock.Stop();            elapsedTimeLabel.Text = clock.ElapsedMilliseconds.ToString("n");        }

Demo 5 {TASK class}

Page 55: News40 Parallel Computing

55

Demo 5 {TASK class}

        private void ApplyFilterImage1()        {            Bitmap b = (Bitmap)System.Drawing.Image.FromFile( Server.MapPath("~/Images/Penguins.jpg"));            b.Invert().Save(Server.MapPath("~/Images/PenguinsInvert.jpg"));            img1.ImageUrl = "~/Images/PenguinsInvert.jpg";        }        private void ApplyFilterImage2()        {            Bitmap b = (Bitmap)System.Drawing.Image.FromFile( Server.MapPath("~/Images/Desert.jpg"));            b.Grayscale().Save(Server.MapPath("~/Images/DesertGrayscale.jpg"));            img2.ImageUrl = "~/Images/DesertGrayscale.jpg";        }

Page 56: News40 Parallel Computing

56

Demo 5 online{TASK class}

Page 57: News40 Parallel Computing

57

Demo 6 {Thread.Priority}

• Pregunta: ¿Es posible establecer la prioridad de un thread en tiempo de ejecución?

• Respuesta: SI en función del entorno, pero en un entorno multicore potente no tiene demasiado sentido, ya que el S.O. siempre se reserva la opción de modificarla.

• Aviso: Existen detractores de cambiar la prioridad a nivel de thread: stackoverflow.com , codinghorror.com

Page 58: News40 Parallel Computing

58

Demo 6 {Thread.Priority}        public long StartCount(ThreadPriority threadpriority)        {            Thread.CurrentThread.Priority = threadpriority;            long threadCount = 0;            while (LoopSwitch)            {                Thread.Sleep(1);                threadCount++;            }            return threadCount;        }

    public enum ThreadPriority    {                Lowest = 0,        BelowNormal = 1,        Normal = 2,        AboveNormal = 3,        Highest = 4,    }

Page 59: News40 Parallel Computing

59

Demo 6 {Thread.Priority}

            ThreadPriority rabbitPriority = (ThreadPriority)DropDownList1.SelectedIndex;

            ThreadPriority turtlePriority = (ThreadPriority)DropDownList2.SelectedIndex;

            Task<long> t1 = Task.Factory.StartNew<long>                (() => priorityTest.StartCount(rabbitPriority));            Task<long> t2 = Task.Factory.StartNew<long>                (() => priorityTest.StartCount(turtlePriority));

Page 60: News40 Parallel Computing

60

Demo 6 online{Thread.Priority}

Page 61: News40 Parallel Computing

61

Demo 7 {Concurrence}

• En un entorno concurrente es bastante probable encontrarse con situaciones de bloqueos, o de elementos eliminados por otros threads.

• NET 4.0 proviene de un conjunto de colecciones specializadas thread-safe: System.Collections.Concurrent

Page 62: News40 Parallel Computing

62

Demo 7 {Concurrence}

• Mostrar un ejemplo (exagerado!) de cómo en un entorno multithread, los elementos de una colección pueden ser modificados o eliminados por otro thread y provocar un error en tiempo de ejecución.

• Para prevenir estos errores veremos como usar un ConcurrentDictionary y su método TryUpdate.

Page 63: News40 Parallel Computing

63

Demo 7 {Concurrence}

        Dictionary<int, Point> points1 = new Dictionary<int, Point>();        ConcurrentDictionary<int, Point> points2 = 

new ConcurrentDictionary<int, Point>();

        private void FillCollection()        {            points1.Clear();            points2.Clear();            Random r = new Random(DateTime.Today.Millisecond);            for (int i = 0; i < 100; i++)            {                int v = r.Next(1, 25);                points1.Add(i, new Point(v, v));                points2.TryAdd(i, new Point(v, v));            }        }

1 (20, 5)

2 (11,16)

3 (3,6)

4 (8,2)

5 (19,25)

Page 64: News40 Parallel Computing

64

Demo 7 {Concurrence}

Lanzamos dos tareas en paralelo, la primera intenta modificar un elemento del diccionario (5), mientras que la segunda borra todos los elementos del diccionario:

protected void Button1_Click(object sender, EventArgs e)

{ FillCollection(); Task t1 = new Task(() => UpdateDictionary()); t1.Start(); Task t2 = new Task(() => ClearDictionary()); t2.Start(); Task.WaitAll(t1, t2); }

Page 65: News40 Parallel Computing

65

Demo 7 {Concurrence}        private void UpdateDictionary()        {            if (points1.ContainsKey(5))            {                Thread.Sleep(2000);                try                {                    Point p = points1[5]; //<- ERROR!!!                    p.X = 666;                    p.Y = 666;                    messagesListBox.Items.Insert(0,                        string.Format("Changed successfully: p.X = {0}", p.X));                }                catch (Exception ex)                {                    messagesListBox.Items.Insert(0,                        string.Format("Error, {0}", ex.Message));                }            }        }

Page 66: News40 Parallel Computing

66

Demo 7 {Concurrence}

private void UpdateConcurrentDictionary() { Point oldp = points2[5]; Point newp = new Point(666, 666); if (points2.TryUpdate(5, newp, oldp)) { messagesListBox.Items.Insert(0, string.Format(

"Changed successfully: p.X = {0}", newp.X)); } else { messagesListBox.Items.Insert(0, string.Format("Error, key and value not found!")); } }

Page 67: News40 Parallel Computing

67

Demo 7 online{Concurrence}

Page 69: News40 Parallel Computing

69

That’s all folks!

Dubtes? Dudas? Doubts?

Lluís [email protected]