martes, diciembre 16, 2008

Libro sobre concurrencia

Llevaba anunciándose desde hace unos cuantos meses, y finalmente está disponible: Concurrent programming on Windows, de Joe Duffy, quien, por cierto, nada tiene que ver con la señorita que canta aquello de "You got me begging you for mercy"...
El libro entra por derecho propio en la categoría tocho, en el buen sentido de la palabra. Si tiene un poco de cuidado con los testigos potenciales, puede arrojarlo desde una primera planta sobre cualquiera de sus enemigos. Asegúrese antes, sin embargo, de que el destinatario no lleva casco.
Observe que la sigla .NET no aparece en el título: en realidad, el libro trata sobre concurrencia utilizando el API nativo de Windows y también .NET. Esa es la buena noticia. La mala es que las partes que tratan sobre .NET suelen ir después de las disertaciones sobre el API nativo, y confieso que llego con sueño a la fiesta.
Si usted ya tiene experiencia con hilos y sincronización, puede que el libro no le enseñe muchas cosas nuevas: la culpa no es del autor, por supuesto, sino que a veces en realidad no hay "más allá". Me ocurría, en su momento, con InterBase: muchas personas me preguntaban por qué no escribía un libro sólo sobre InterBase... y la respuesta era y es que no había mucho de lo que escribir.

Etiquetas: , , , ,

martes, noviembre 11, 2008

Antología del Disparate - II

Advertencia: Cualquier parecido con
la realidad, es sólo una sospechosa coincidencia...
Boxingstamos otra vez en una aplicación que maneja un buen puñado de hilos. El "genio" que la programa ya ha tomado consciencia de los peligros de la falta de sincronización, y dedica su tiempo libre a repartir bloqueos a diestra y siniestra. Este es un bello ejemplo de cómo se las gasta el caballerete (imaginario, of course):
private bool flag;
 
public void YoQueSe()
{
if (Monitor.TryEnter(flag))
{
// Código "sincronizado" ...
Monitor.Exit(flag);
}
}
¿Puede ver los disparates? Son varios, por supuesto. ¿Cómo cree que se manifestó el problema? ¿Se atreve a imaginar cómo se le pudo haber ocurrido esta barbaridad a su perpetrador?

Esta es, amigo mío, la declaración del método TryEnter de la clase Monitor:
public static bool TryEnter(object obj);
Como puede ver, el parámetro del método debe ser un Object, o más exactamente, una referencia a un objeto. Como Object es la clase base final del CLR, cualquier cosa que pasemos como parámetro servirá, a efectos del sistema de tipos. El método incluso admite que pasemos un valor de tipo Boolean...
... con la única pega de que Boolean es una estructura, un tipo con semántica de asignación por valor. Para poder pasar un Boolean donde se espera un objeto, .NET aplica la operación llamada boxing al valor. Es decir, crea un objeto temporal del tamaño adecuado, y copia en su interior el valor que queríamos pasar. ¿No queríamos una referencia? Tras el boxing ya tenemos una.
¿Y qué hay con la llamada final a Exit? Este es el prototipo de este segundo método:
public static void Exit(object obj);
Nuevamente, nos piden un objeto. Y el compilador no protesta si pasamos un valor de tipo Boolean. Simplemente mete el valor en una "cajita" y pasa el puntero a ésta. ¿La pena? Pues que las llamadas a TryEnter y Exit deberían utilizar el mismo objeto... y cada vez que se realiza el boxing, ¡se crea una instancia temporal diferente!
¿Qué consecuencias cree que tenía el disparate? Sorprendentemente, ¡ninguna!... al menos, mientras la aplicación se mantuvo ejecutando sobre .NET v1.1. Fue al migrar a .NET 3.5 que estos errores empezaron a ser detectados en tiempo de depuración. La llamada a Exit comenzó a generar una excepción, advirtiendo que se intentaba desbloquear a través de un objeto no bloqueado. Claro, sería una estupidez que el CLR tuviese que verificar en cada llamada a Exit que el objeto pasado no sea una instancia creada por boxing: penalizaría a los buenos programadores para evitar que los peores metiesen la pata.
¿Se da cuenta de que, ni siquiera en .NET v1.1, la aplicación estaba protegiendo la sección de código de marras? Si en algún momento se produjeron errores de sincronización en dicha sección, o fueron ignorados... o se perdieron gracias a la demencial política de manejos de excepciones de mi aplicación imaginaria. Observe que hay un segundo error en el código mostrado: se supone que las llamadas a TryEnter y Exit deberían estar asociadas a una instrucción try/finally.
Y ahora viene lo mejor: ¿cómo imagina que se le ocurrió la burrada al genio? El uso de TryEnter nos permite reconstruir los hechos. En realidad, el aprendiz de brujo intentó usar la instrucción lock en primer lugar. ¡Pero lock sí que puede detectar cuando alguien le pasa una estructura en vez de una referencia! Y como lock no se dejaba violentar, el muy pillo tradujo el código que suponía que se generaba para dicha instrucción. Pero como manazas que es, incluso eso lo hizo mal...
Moraleja: odio repetirme pero, por favor, esos genios que deambulan por ahí, ¡RTFM!

Etiquetas: , , , , ,

lunes, noviembre 10, 2008

Antología del Disparate - I

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: , , , ,