Advertencia: Cualquier parecido con
la realidad, es una cochina casualidad...
n buen señor, presuntamente un experto en casi todo, bien pagado y considerado, recibe el encargo de crear una "base de datos en memoria"... tarea que, para el resto de nosotros, los mortales, consiste en meter objetos en un contenedor. Monta su colección basada en
ICollection (estamos en los remotos tiempos de .NET v1.1) y añade internamente un par de
Hashtable's para permitir el acceso directo dada una clave. Como las "cosas" que tiene que guardar en el contenedor no tienen una clave primaria clara, decide con buen tino añadir una clave artificial, que implementa de la siguiente manera:
public class Cosa
{
private static int staticId;
private int instanceId;
// ...
}
El campo estático staticId contiene el valor a asignar a la próxima "cosa" que se cree, mientras que instanceId representa la clave única y artificial de cada instancia. Esta clave, como sospechará, se asigna automáticamente durante la construcción de instancias.
El problema es que la aplicación en cuestión consume hilos a manos llenas, y es posible que se creen dos "cosas" a la misma vez. Nuestro sujeto, que no es del todo lerdo, sabe que tiene que protegerse, y diseña el siguiente constructor:
public Cosa()
{
// ¡INCORRECTO!
Interlocked.Increment(ref staticId);
instanceId = staticId;
}
¿Dónde está el disparate? ¿Cómo, y cuándo, cree usted que se manifestará? ¿Cómo se puede corregir?
El disparate, evidentemente, está en el uso incorrecto de la clase
Interlocked. Se trata, en realidad, de un caso flagrante de
cargocultismo. El "experto" conocía un par de datos correctos:
- El patrón de asignación de un identificador autoincremental exige el uso de primitivas de sincronización, o de lo contrario, pueden producirse anomalías.
- La clase Interlocked permite sustituir determinados usos de la instrucción lock de forma más eficiente.
Hasta ahí, todo bien: el experto había oído sonar campanas. Lo malo es que no sabía dónde. No se tomó la molestia de leerse hasta el final la ayuda de Interlocked (¿para qué?). Peor aún: no se preguntó cómo demonios se las arregla esta clase para que el código sea equivalente a un lock en toda regla. El concepto de "actualización atómica" le resultó tan extraño como escuchar a un marine hablar con Alpha Bravo Charlie a través de su mágica radio-cocotero.
Para explicar qué es lo que está mal en el código original, es mejor que le presente el código correcto:
public Cosa()
{
// ¡CORRECTO!
instanceId = Interlocked.Increment(ref staticId);
}
Como ve, el arreglo es sencillo: hay que asignar a instanceId el resultado de la llamada a Interlocked.Increment, en vez de desperdigar la llamada y la asignación entre dos instrucciones; enseguida veremos por qué. En caso de que no existiese Interlocked, la forma correcta de realizar esta operación sería:
public Cosa()
{
lock (typeof(Cosa))
instanceId = ++staticId;
}
Los más puristas verán con malos ojos el bloqueo sobre el typeof, pero lo he escrito así para abreviar. Pues bien: el código anterior es equivalente al código correcto. El motivo por el que usamos lock es evitar que, entre la modificación de la variable global y la asignación de su valor en el campo de la instancia pueda cederse el control a otro hilo. Si esto ocurriese, podría ser que recibiésemos un valor duplicado en instanceId. La clase Interlocked, precisamente, nos garantiza que el incremento de la variable estática y la copia del valor correcto como valor de retorno tengan lugar de forma atómica, sin posibilidad de que otro hilo pueda acceder a la variable estática modificada. La llamada a Increment, por otra parte, es mucho más eficiente que el uso de lock.
Por el contrario, en el código erróneo original, el "genio" ha roto la atomicidad de la operación... por no tomarse la molestia de examinar bien la documentación o meditar sobre el problema. ¿Cuándo se manifestó el error? Esta es la parte que suele infundir horror en los corazones sensibles: el error fue detectado casi por casualidad:
- Por una parte, para que se produzca el error, tienen que intercalarse las operaciones de dos hilos de manera muy especial. Estos problemas no se detectan con una simple ejecución... si no hay suerte.
- Para rematar, y esto es tan grave como todo lo anterior, la aplicación tenía un sistema de "manejo" de errores demencial, que se tragaba todas las excepciones gracias a cantidades industriales de instrucciones try/catch repartidas uniformemente a lo largo y ancho del código. En caso de producirse un error, la aplicación se lo callaba taimadamente.
Un buen día, fue necesario sacar una copia de la colección de "cosas" en un dataset vulgar y corriente... y ahí fue que saltó la violación de la unicidad.
Y usted quizás se pregunte si mi personaje imaginario (¡porque cualquier parecido con la realidad... ya sabe!) se merece que lo vapulee de este modo. Pues sí, se lo merece. En este caso, el resultado de su metedura de pata es que, en ocasiones, la aplicación veía muertos y perdía registros fantasmas. Nada grave: sólo se trataba de dinero. Imagine, en cambio, que la aplicación controlase aviones, o trenes de pasajeros, o que monitorizase constantes vitales en un hospital. ¡Todos a la cárcel! Hace mucho, migrando una aplicación de altas hospitalarias desde dBase a InterBase, tropecé con el expediente médico de un caballero al que habían, aparentemente, operado de los ovarios. En realidad, por un error parecido, la clave primaria del expediente de este señor coincidía con la clave del expediente de una mujer, y los registros de detalles eran compartidos por ambos expedientes.
Moraleja: las optimizaciones son buenas. Pero, ¡por favor!, léase bien el manual antes de experimentar con métodos que no conoce.
Etiquetas: C#, CLR, disparates, hilos, sincronización