Usando System.Collections.Concurrent.ConcurrentBag - Jorge Serrano - MVP Visual Developer - Visual Basic

Usando System.Collections.Concurrent.ConcurrentBag

Introducción:

Hoy voy a hablar de un namespace que fué introducido en .NET Framework 4.0 y del cual no he oído hablar mucho, me refiero a System.Collections.Concurrent.

Antes de hablar de este namespace imaginemos la siguiente situación:

Tenemos una caja dentro de la cual vamos colocando diferentes elementos uno detrás de otro, si bien, el orden en el que llegan esos elementos no es en este caso lo más importante para nosotros.

Ahora imaginemos varios procesos concurrentes accediendo a esa caja para obtener un elemento (y sólo uno), sacarlo de la caja y trabajar y operar con él.

Cuando uno de esos procesos finaliza, acudiría nuevamente a la caja en busca de más elementos obteniendo nuevamente un único elemento, realizando la misma operación (obtención del elemento y trabajo con él).

System.Collections.Concurrent:

Antes de que Microsoft introdujera en .NET Framework 4.0 las colecciones de las que voy a hablar y las cuales resolverían este problema, yo me hice mis propias clases (anti-colisión) que utilizaba a modo de listener una colección genérica que permitía consultar sus elementos y extraerlos de la colección, todo esto desde varios procesos o hebras concurrentes.

Aunque mis clases funcionan aún hoy perfectamente, no es menos cierto que si tenemos esto ya en .NET Framework, ¿para qué complicarse la vida?.

Así que con esta idea quiero hacer mención a System.Collections.Concurrent en esta entrada, para que tengamos en cuenta el uso de este namespace y su posible reutilización.

Hablando de System.Collections.Concurrent, diremos que contiene un conjunto de interesantes clases que nos permitirán implementar diferentes tipos de colecciones de una forma segura evitando colisiones.

Entre las clases que encontraremos en System.Collections.Concurrent haré una mención especial a la clase ConcurrentBag<T> que como veréis es genérica e implementa esta funcionalidad de forma rápida y sencilla.

No obstante, ConcurrentBag<T> tiene varias particularidades.
se trata de una clase en modo LIFO.
Si queremos trabajar con datos en modo FIFO, deberemos pensar en trabajar con ConcurrentQueue<T>, que en mi caso coincide más con las clases que me construí antes de .NET Framework 4.0 para trabajar con colas de mensaje y de datos concurrentemente.
Otra clase interesante es ConcurrectStack<T> que permite trabajar con los datos en modo LIFO también, si bien en este caso nos permite obtener no un elemento por llamada, sino incluso más en caso de ser necesario.

Pero dentro de System.Collections.Concurrent también podemos encontrar una clase algo más especial.
BlockingCollection<T> nos permite introducir características de bloqueo para nuestras colecciones.

Sin embargo, y pese a que el uso de todas las colecciones es muy similar y sencillo, vamos a fijarnos en ConcurrentBag<T> para mostrar su funcionamiento y entenderlo bien (es muy sencillo la verdad).

La demostración de ConcurrentBag<T>:

Para mostrar el uso de esta clase, he decidido crear un sencillo ejemplo de código en C# que realiza las siguientes tareas:

- Creamos una clase Person con una propiedad (Name).
- Cargamos una colección de objetos Person (25 en concreto).
- Cada objeto Person formará parte de una colección de tipo ConcurrentBag<Person>.
- Creamos 3 controles Timer con un intervalo de 500 ms, 750 ms y 1000 ms que se lanzarán cuando le toque a cada uno.
- Cada timer obtendrá un elemento de ConcurrentBag y mostrará su contenido en un control textBox.

Nuestra aplicación de demostración en tiempo de ejecución es la que se indica en la siguiente imagen:

El objetivo de este ejemplo es únicamente demostrar el funcionamiento de ConcurrentBag<T>, por lo que obviado otras particularidades que tendrían que tenerse en cuenta en entornos empresariales y de producción.

Como podremos observar, agregar un elemento a la colección se realiza con el método Add.
Mientras que obtener un elemento de la colección se realiza con el método TryTake. TryTake tiene la particularidad de que además de obtener un elemento de la lista o colección de elementos, lo elimina de la misma.
Si quisiéramos obtener un elemento de la colección pero no eliminarlo, deberíamos utilizar TryPeek.
Ahora bien, en el caso del ejemplo TryPeek haría que el lanzamiento de los tres eventos timer siempre obtuviera el último elemento, algo que en esta demostración no es lo buscado.

Volviendo a TryTake, lo bueno de esta función es que nos devuelve true en caso de tener datos y false en caso contrario.

No obstante, en este ejemplo obtengo directamente el elemento de tipo Person.
He reducido el ejemplo a la mínima expresión y he asumido que no hay elementos de tipo null en la colección, así que si el elemento devuelto por TryTake es null es que no hay más elementos en la colección.

Pero recordad que lo ideal es preguntar por su valor (true/false) para saber si hay más elementos en la colección.
También tenemos no obstante la propiedad Count que siempre es una buena aliada, y con esto doy la pista también para resolver el posible problema de un elemento de la colección a null (todo esto desde el punto de vista del ejemplo de demostración... el código en sí aclarará más esto que comento).

Y ahora el código de esta pequeña demostración del uso de System.Collections.Concurrent.ConcurrentBag<T>.

Código fuente:

Person.cs

   1: namespace ConcurrentBagSample
   2: {
   3:  
   4:     public class Person
   5:     {
   6:  
   7:         /// <summary>
   8:         /// Inicializa una nueva instancia de la clase <see cref="T:Person"/>.
   9:         /// </summary>
  10:         /// <remarks>
  11:         /// Constructor de la clase Person.
  12:         /// </remarks>
  13:         public Person(string name)
  14:         {
  15:             this.Name = name;
  16:         } // Person Constructor
  17:  
  18:         public string Name { get; set; }
  19:  
  20:     } // Person
  21:  
  22: } // ConcurrentBagSample

 

MainForm.cs

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4: using System.Windows.Forms;
   5:  
   6: namespace ConcurrentBagSample
   7: {
   8:  
   9:     public partial class MainForm : Form
  10:     {
  11:  
  12:         public MainForm()
  13:         {
  14:             InitializeComponent();
  15:         } // MainForm
  16:  
  17:  
  18:         private void MainForm_Load(object sender, EventArgs e)
  19:         {
  20:             // Indicamos el número de elementos que cargaremos.
  21:             this.NumberOfElements = 25;
  22:             // Indicamos en el control de progreso el valor máximo que
  23:             // corresponde con el de los elementos.
  24:             this.progressBar1.Maximum = this.NumberOfElements;
  25:             this.progressBar1.Value = 0;
  26:             // Inicializamos la colección de elementos concurrentes.
  27:             this.PersonConcurrentCollection = new System.Collections.Concurrent.ConcurrentBag<Person>();
  28:             // Cargamos los datos con los que trabajaremos.
  29:             LoadData();
  30:         } // MainForm_Load
  31:  
  32:  
  33:         public System.Collections.Concurrent.ConcurrentBag<Person> PersonConcurrentCollection { get; set; }
  34:  
  35:         public int NumberOfElements { get; set; }
  36:  
  37:         private void LoadData()
  38:         {
  39:             // Cargamos un conjunto de datos a modo de simulación.
  40:             // Estos datos nos permitirán comprender mejor el funcionamiento 
  41:             // de ConcurrentBag.
  42:             for (int i = 0; i < this.NumberOfElements; i++)
  43:             {
  44:                 PersonConcurrentCollection.Add(new Person(string.Format("Person {0}", i)));
  45:             }
  46:         } // LoadData
  47:  
  48:  
  49:         private void button1_Click(object sender, EventArgs e)
  50:         {
  51:             // Mostramos el número de elementos de la colección...
  52:             this.label4.Text = string.Format("Initial elements: ({0})", this.PersonConcurrentCollection.Count.ToString());
  53:             // Comenzamos a recuperar datos...
  54:             this.timer1.Enabled = true;
  55:             this.timer2.Enabled = true;
  56:             this.timer3.Enabled = true;
  57:         } // button1_Click
  58:  
  59:  
  60:         private void timer1_Tick(object sender, EventArgs e)
  61:         {
  62:             Person person = null;
  63:             GetData(ref person);
  64:             if (person != null)
  65:             {
  66:                 this.textBox1.Text += person.Name + Environment.NewLine;
  67:             }
  68:             else
  69:             {
  70:                 this.timer1.Enabled = false;
  71:             }
  72:             // Mostramos el avance del proceso.
  73:             ShowAdvance();
  74:         } // timer1_Tick
  75:  
  76:  
  77:         private void timer2_Tick(object sender, EventArgs e)
  78:         {
  79:             Person person = null;
  80:             GetData(ref person);
  81:             if (person != null)
  82:             {
  83:                 this.textBox2.Text += person.Name + Environment.NewLine;
  84:             }
  85:             else
  86:             {
  87:                 this.timer2.Enabled = false;
  88:             }
  89:             // Mostramos el avance del proceso.
  90:             ShowAdvance();
  91:         } // timer2_Tick
  92:  
  93:  
  94:         private void timer3_Tick(object sender, EventArgs e)
  95:         {
  96:             Person person = null;
  97:             GetData(ref person);
  98:             if (person != null)
  99:             {
 100:                 this.textBox3.Text += person.Name + Environment.NewLine;
 101:             }
 102:             else
 103:             {
 104:                 this.timer3.Enabled = false;
 105:             }
 106:             // Mostramos el avance del proceso.
 107:             ShowAdvance();
 108:         } // timer3_Tick
 109:  
 110:  
 111:         private void GetData(ref Person person)
 112:         {
 113:             // Obtenemos un elemento de la colección, eliminándola de 
 114:             // la colección en sí.
 115:             this.PersonConcurrentCollection.TryTake(out person);
 116:         } // GetData
 117:  
 118:  
 119:         private void ShowAdvance()
 120:         {
 121:             // Indicamos el avance en la barra de progreso.
 122:             this.progressBar1.Value = this.NumberOfElements - this.PersonConcurrentCollection.Count;
 123:             // Mostramos el número de elementos de la colección...
 124:             this.label5.Text = string.Format("Elements to read: ({0})", this.PersonConcurrentCollection.Count.ToString());
 125:         } // ShowAdvance
 126:  
 127:     } // MainForm
 128:  
 129: } // ConcurrentBagSample

Espero que le sirva a más de uno y que le haga comprender la utilidad de lo que contiene System.Collections.Concurrent.

Referencias:

.NET Framework 4.0 :: System.Collections.Concurrent

Published 22/8/2011 14:00 por Jorge Serrano
Comparte este post:

Comentarios

Tuesday, August 23, 2011 8:53 AM por Javier Torrecilla

# re: Usando System.Collections.Concurrent.ConcurrentBag

Buenos días!!!

He estado probando todo y la verdad es que funciona genial!!

Haciendo pruebas entre las distintas colleciones (ConcurrentBag, ConcurrentQueue y ConcurrentStack) he visto que solo en ConcurrentStack hay un método que es: TryPopRange

Tiene 2 sobrecargas, y puede resultar interesante:

la primera sobrecarga recibe un Array del tipo indicado por el tipo de la colección, y la segunda sobrecarga recibe el array, el indice donde empezar a coger elementos y el numero de elementos a coger!

msdn.microsoft.com/.../dd381781.aspx

Great Post Jorge!

Saludos

Tuesday, August 23, 2011 10:31 AM por Lluis Franco

# re: Usando System.Collections.Concurrent.ConcurrentBag

Muy buen post Jorge.

Es curioso como la TPL ha pasado bastante desapercibida, cuando para mi es de lo mejorcito de .NET 4.0 :-)

PD - Sólo un apunte: ¿Y esos frames tan pequeñitos para el código? :-P

Tuesday, August 30, 2011 11:12 AM por Juanma

# re: Usando System.Collections.Concurrent.ConcurrentBag

Muy interesante, aunque no tengo muy claro que el ejemplo sea correcto.

Aparentemente los timers que estas usando son de System.Windows.Forms, y esos timers lo que hacen es mandar mensajes WM_TIMER que se ejecutan todos en la misma hebra (la de UI), por lo que creo que no estás probando realmente la concurrencia.

Tuesday, August 30, 2011 2:54 PM por Jorge Serrano

# re: Usando System.Collections.Concurrent.ConcurrentBag

Hola Juanma, muchas gracias por comentar.

Tienes razón en cuanto a System.Windows.Forms y su ejecución en la misma hebra, de hecho, es por eso que cuando intentamos utilizar multithreading y tenemos controles de WinForms, debemos acceder a ellos de forma especial y no directamente como accederíamos para modificar cualquier propiedad del mismo.

Si no estoy equivocado, el control System.Windows.Forms.Timer no crea un pool de hebras. De hecho, el control se basa en WM_TIMER y encola las diferentes llamadas o mensajes de WM_TIMER según sus intervalos, es decir, NO ES CONCURRENCIA (tienes razón).

No obstante, el ejemplo era para mostrar la teoría del funcionamiento de System.Collections.Concurrent.ConcurrentBag<T>. Está claro que no es el mejor ejemplo como bien has apuntado, pero funciona perfectamente tal y como indico.

Si tengo tiempo esta semana, pongo un ejemplo con Threads para demostrar que funciona perfectamente tal y como se espera. ;-)

Un saludo y gracias por aclarar esto.

Jorge

Sunday, September 04, 2011 7:42 PM por Juanma

# re: Usando System.Collections.Concurrent.ConcurrentBag

Hola Jorge,

Aprovechando esto he preparado un post que explica un poco como funciona el tema de mensajes en Windows, sobre todo para los más jóvenes que han tenido la suerte de no verlos muy de cerca :-)

Si a alguno os interesa está aquí: http://blog.koalite.com/?p=215

Saludos.