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:
5 Responsesso far
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!
http://msdn.microsoft.com/es-es/library/dd381781.aspx
Great Post Jorge!
Saludos
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? 😛
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.
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. 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
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.