Evolución del Thread Local Storage (a.k.a. TLS)

Una de las cosas que más me gusta de .NET es lo rápido y consistente que evoluciona. Cuando cubre alguna funcionalidad, lo hace de manera elegante y bien terminada. Como desarrolladores es un gusto consumir las clases que exponen dicha funcionalidad y la experiencia suele ser de lo mas gratificante.

Nuestra responsabilidad como desarrolladores es conocer lo que nos brinda y aprovechar al máximo las ventajas que dichas evoluciones nos aportan. Esto nos permite centrar nuestros esfuerzos en las tareas que más valor aportan delegando en el framework las tareas más rutinarias y que aun siendo necesarias no dan valor al producto.

Un ejemplo de estas mejoras, incluida en el Framework 4 cubre un escenario más común de lo que parece, como es implementar un thread-local storage, conocido como TLS.

Imaginemos que deseamos realizar ciertas tareas en paralelo. Común, verdad? Además imaginemos que para el correcto procesamiento de esas instrucciones cada uno de los hilos necesita manejar sus datos de manera separada de los demás hilo de procesamiento que ejecuta un mismo código. Esto es, cada hilo en el que se están procesando las instrucciones, dispone de una copia propia y separada de los recursos que necesita para dicho procesamiento.

Esta característica es interesante si por ejemplo exponemos cierta funcionalidad a través de un Singleton (o un conjunto determinado de instancias), pero necesitamos permitir que dicho código pueda ser invocado desde diferentes hilos. También puede ser interesante si trabajamos con algún recurso no manejado que no soporte MTA (como la implementación del COM Interop de Silverlight, por ejemplo…) o si tenemos un código heredado que deseamos modificarlo para soportar ciertas características de procesamiento en paralelo, aplicando los mínimos cambios posibles.

Para cubrir esta funcionalidad desde el .NET Framework 1.1 existe el atributo [ThreadStatic] que brinda una forma elegante de implementarlo.

Veamos con un poco de código:

  1: ...
  2: 
  3: internal class TLSTestClass
  4: {
  5:     private ClassWithHeavyProcess tp;
  6: 
  7:     internal void Test()
  8:     {
  9:         //UI Thread
 10:         tp = new ClassWithHeavyProcess();
 11:         tp.HeavyMethod();
 12: 
 13:         var bw = new BackgroundWorker();
 14:         bw.DoWork += BwDoWork;
 15:         bw.RunWorkerAsync();
 16: 
 17:         var bw2 = new BackgroundWorker();
 18:         bw2.DoWork += Bw2DoWork;
 19:         bw2.RunWorkerAsync();
 20:     }
 21: 
 22:     void BwDoWork(object sender, DoWorkEventArgs e)
 23:     {
 24:         tp.HeavyMethod();
 25:     }
 26: 
 27:     void Bw2DoWork(object sender, DoWorkEventArgs e)
 28:     {
 29:         tp.HeavyMethod();
 30:     }
 31: }
 32: ...

Veamos como implementar la clase ClassWithHeavyProcess.

Como primera aproximación podemos hacer algo así:

  1:  internal class ClassWithHeavyProcess
  2:     {
  3:         [ThreadStatic]
  4:         public static NeededForThreadProcess _neededForThreadProcess;
  5:         
  6:         static ClassWithHeavyProcess()
  7:         {
  8:             _neededForThreadProcess = new NeededForThreadProcess { Name = "Andoni" };
  9:         }
 10: 
 11:         public void HeavyMethod()
 12:         {
 13:             _neededForThreadProcess.Name = "Assigned from the thread with id: " + Thread.CurrentThread.ManagedThreadId;
 14:         }
 15:     }

Uno de los problemas de esta implementación, es que la inicialización de la variable, al estar implementada en el constructor estático solo se ejecuta una vez. Este escenario nos funcionará solo para el primer hilo, puesto que todos los demás accederán a dicha variable con un valor por defecto del tipo (nulo en los tipos por referencia).

Si no tenemos en cuenta esta circunstancia podemos obtener una bonita System.NullReferenceException como la obtiene el código aquí mostrado…

Como a grandes males, grandes remedios, podemos crearnos una propiedad que encapsule el acceso al campo e inicializarlo si está a nulo, es decir, si estamos en un hilo que no posee su propia copia de la variable.

Entonces tendríamos algo así:

  1:  internal class ClassWithHeavyProcess
  2:     {
  3:         [ThreadStatic]
  4:         public static NeededForThreadProcess _neededForThreadProcess;
  5: 
  6:         public NeededForThreadProcess NeededForThreadProcess
  7:         {
  8:             get
  9:             {
 10:                 if (_neededForThreadProcess == null)
 11:                     _neededForThreadProcess = new NeededForThreadProcess();
 12:                 return _neededForThreadProcess;
 13:             }
 14:         }
 15: 
 16:         public void HeavyMethod()
 17:         {
 18:             NeededForThreadProcess.Name = "Assigned from the thread with id: " + Thread.CurrentThread.ManagedThreadId;
 19:         }
 20:     }

Tened en cuenta que cuando un hilo completa su procesamiento es eliminado y liberados sus recursos, incluida la copia de la variable asociada. (Si el GC ha decidido llevárselos al más allá…).

Bueno, esto es así casi siempre…

Recordad que desde el .NET Framework 2.0 existe el ThreadPool. De las optimizaciones que el .NET Framework haga sobre el pool de hilos manejados también nuestras aplicaciones se verán beneficiadas, puesto que si reutilizamos los hilos también sus variables de almacenamiento local. Esto es una mejora en el rendimiento, pero debemos asegurarnos que inicializamos correctamente las variables al inicio del procesamiento, puesto que pueden ser parte de algún hilo reciclado.

Si os preguntáis porque os cuento esto, tened en cuenta que si revisáis la implementación de la clase TLSTestClass podéis comprobar que está empleando objetos BackgroundWorker  que internamente negocian con el ThreadPool de .NET.

Con respecto a este escenario el .NET Framework 4 ha evolucionado para hacernos las cosas aún más fáciles. Se ha incluido la clase genérica ThreadLocal (que ya existía en Java, sí…) que se encarga de encapsular el poco “pipeline” que nos quedaba por hacer a nosotros.

Con ella podemos evitar la propiedad y especificar el constructor que deberá invocar cuando lo necesite.

Con esta clase nuestro código podría quedar tan elegante como este:

  1:  internal class ThreadProcess
  2:     {
  3:         ThreadLocal<NeededForThreadProcess> NeededForThreadProcess = new ThreadLocal<NeededForThreadProcess>(() => new NeededForThreadProcess());
  4: 
  5:         public void HeavyMethod()
  6:         {
  7:             NeededForThreadProcess.Value.Name = "Assigned from the thread with id: " + Thread.CurrentThread.ManagedThreadId;
  8:         }
  9:     }

Realmente elegante!

Si implementáis esta funcionalidad porque desde la clase NeededForThreadProcess gestionáis algún recurso no manejado (uno de los escenarios que comentaba antes), recordad que es responsabilidad vuestra liberad dichos objetos. Esto lo debéis realizar al menos al destruir los objetos (sino antes) que los consumen, pero eso, como el valor en la mili, se os supone..