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