Hebras, lambdas… y bugs

Cuando uno empieza a mezclar ciertas cosas tiene que andarse con bastante ojo para no liarla. La programación concurrente es una de esas cosas que, en cuanto requiere compartir información entre procesos/hebras (de la forma que sea), se convierte en algo que hay que tratar con cariño para evitar introducir condiciones de carrera (race conditions).

Las hebras

Una condición de carrera (tuve un profesor que la llamaba carrera crítica, que me parece un nombre mucho más bonito), se produce cuando dos hebras ejecutan una secuencia de instrucciones en un orden que no teníamos previsto, lo que genera fallos bastante difíciles de reproducir. El caso más habitual son dos hebras accediendo a una misma variable y “pisándose” el valor que acaban de escribir.

Todo esto viene a cuento porque hace poco tuve que depurar un fragmento de código con una pinta parecida a ésta:

public class Wrapper
{
	private int current;
	private int total;
	private Control control;

	// Más atributos, propiedades, métodos...

	public void Start(int param1, int param2)
	{
		SomeFunctionInBackgroundThread(
			param1, param2,
			(newCurrent, newTotal) => 
			{
				this.current = newCurrent;
				this.total = newTotal;

				// Unas cuantas cosas más...

				if (this.total > 0)
					control.BeginInvoke((Action)(() =>control.Text = (current/total).ToString("p")));
			});
	}
}

En el método Start se invocaba una función en una dll nativa a la que se le pasaba una función callback que era invocada cada cierto tiempo para informar del progreso. La función callback lo único que hace es actualizar los valores almacenados con el estado del progreso y actualizar el texto de un control, para lo que necesita usar BeginInvoke y redirigir la llamada a la hebra de interfaz de usuario.

Cuando acaba el proceso, la dll nativa invocaba la función callback pasando como parámetro newTotal == 0 para indicar que ha finalizado. Para evitar la división por 0, antes de lanzar el BeginInvoke comprobamos que total > 0 y ya está, ¿no? Pues no, eso no funciona.

Los cierres lambda

Al usar una expresión lambda C# estamos creando un cierre lambda sobre las variables que referenciamos en la expresión. En un cierre lambda, aquellas variables que no son locales a la función (en este caso, a la expresión lambda) son enlazadas a variables del contexto exterior, en este caso, al método que contiene la expresión lambda.

Para invocar BeginInvoke estamos pasando una expresión lambda:

control.BeginInvoke((Action)(() =>control.Text = (current/total).ToString("p")));

A primera vista, podría parecer que el cierre lambda se realiza sobre las variables control, current y total, pero ojo que no se trata de variables locales. Realmente el cierre se realiza sobre la referencia implícita a this, ya que en realidad total es this.total.

¿Y esto que quiere decir? Esto quiere decir que, si mezclamos la forma en que se realiza el cierre lambda con las condiciones de carrera que contábamos antes, tenemos un problema. Puede ser que para cuando se ejecute la expresión pasada a BeginInvoke (que se ejecuta de forma asíncrona) se haya vuelto a invocar la función callback y el valor de total haya pasado a ser 0.

La solución es muy sencilla: se puede meter el if dentro de la expresión lambda o usar las variables locales de la función callback, que como son locales no se van a ver modificadas por las siguientes invocaciones de la función callback, pero me costó un rato verlo.

Moraleja

La programación concurrente no es trivial. Si puedes evitar compartir estado, mejor. Si puedes evitar estado mutable, mejor.

Los cierres lambda son poderosos, pero ten cuidado de estar seguro de lo que cierras. Los bugs derivados de mezclar cierres lambda con programación concurrente son muy divertidos de depurar.

Un comentario en “Hebras, lambdas… y bugs

  1. Pingback: Cómo tirar abajo tu servidor web

Comentarios cerrados.