Más ligero que un bloqueo
Seamos sinceros: si usted necesita un bloqueo, no va a conseguir un recurso menos costoso que el propio bloqueo. Ahora bien, es muy diferente si lo que usted necesita no es exactamente un bloqueo, sino parte de la funcionalidad del bloqueo. En tal caso, hay alternativas.
Hace poco me encontré en esta situación, añadiendo concurrencia a XSight RT. En las últimas versiones (no publicadas todavía) dos hilos pueden ocuparse de una misma escena, generando bandas disjuntas sobre un mismo objeto PixelMap (los mapas de bits internos del API de XSight RT). De este modo, los dos hilos pueden trabajar sin molestarse mutuamente. El conflicto se produce cuando hay que ejecutar un evento para avisar al cliente del API de que hay una banda nueva que se puede mostrar incrementalmente en la ventana de salida. En el código original, sin soporte de hilos, ésta era una tarea del sampler. En el nuevo código, por inercia, este código se duplicó en cada hilo: cada uno intenta notificar al cliente, cuando se ha acumulado trabajo o ha pasado suficiente tiempo.
¿Primera solución? Proteger ese código mediante un bloqueo, por supuesto:
if (SeDanLasCondiciones())
lock
{
Avisar();
}
Esto es matar gorriones a cañonazos. Un bloqueo, implementado aquí mediante la instrucción lock, que el compilador traduce en llamadas a la clase Monitor, se encarga en realidad de dos tareas que se pueden separar:
- Comprobar que el código protegido no pueda ejecutarse simultáneamente por más de un hilo.
- Si un hilo solicita el bloqueo mientras está concedido a otro hilo, el hilo se pone en modo de espera.
Pasar al "modo de espera" puede ser costoso, de acuerdo a la técnica con la que se haya implementado el bloqueo. En el peor de los casos, si se suspende el hilo mediante llamadas al API de Windows, esto provoca un pequeño terremoto en el planificador de tareas: tenga presente que se produce una transición al modo kernel del sistema operativo.
Para su tranquilidad, es muy poco probable que Monitor, y por consiguiente la instrucción lock, implemente la espera de esta manera. Lo más probable es que intente esperar un instante antes de tomar medidas drásticas y tomarse el somnífero.
No obstante, en mi ejemplo, ni siquiera hace falta esperar. ¿Que un hilo intenta notificar y encuentra que alguien se le ha adelantado? ¡Estupendo! ¡Ya no necesita notificar sobre nada! La única parte que necesitamos del bloqueo es la funcionalidad de exclusión mutua... y para esta tarea sencilla, existe un mejor mecanismo. Observe:
private int access;
public bool ReportProgress()
{
int elapsed = Environment.TickCount;
if (elapsed - lastReport > MIN_INTERVAL)
{if (0 == Interlocked.Exchange(ref access, 1))return !progressInfo.CancellationPending;
{
lastReport = elapsed;
listener.Progress(progressInfo);
Interlocked.Exchange(ref access, 0);
}
}
else
return true;
}
Este código está ubicado en la clase Scene, y los dos hilos comparten una instancia de la misma. Por lo tanto, ambos hilos están usando el mismo campo access, declarado dentro de Scene. Este campo se utiliza en la zona resaltada del método, a través de métodos estáticos de la clase Interlocked.
Primero se ejecuta el método Exchange. Este método intercambia el valor almacenado en la variable con el valor que pasamos como parámetro, y nos devuelve el antiguo valor de la variable. Además, nos ofrece la garantía de que todo este meneo lo hará de forma atómica: no se producirán intromisiones durante su ejecución. Nosotros asumiremos que el valor "normal" del campo access es cero. ¿Qué pasa si intentamos intercambiar el cero por un uno, y descubrimos que ya tiene un uno? Simplemente, que alguien se nos ha adelantado. En tal caso, renunciamos a notificar, pues ya alguien se ha ocupado del asunto. De lo contrario, si Exchange devuelve cero, tendremos la seguridad de que nadie más ejecutará la notificación... al menos, mientras no reasignemos un cero en access. Esto es precisamente lo que hacemos al terminar con la notificación, dejando la vía libre para la próxima notificación.
Vea también: Antología del disparate I.