Sobre Threads

Usos y errores

En el post anterior hablamos un poco sobre concurrencia y como su implementación se ha ido mejorando a través del tiempo. Desde procesos hasta threads.

Los threads están presentes en el desarrollo de aplicaciones, inclusive si nunca has creado un thread explícitamente; seguramente alguno de los frameworks que usas utilizan implícitamente threads. Por ejemplo en Java, cualquier programa que corre en la JVM corre a través de threads. Cuando la JVM inicia, crea varios threads para ejecutar ciertas tareas en background (garbage collection, monitoring, schedulers) y también crea un thread para correr el método main que es el punto inicial de tu aplicación.

Independientemente de si tú creas threads explícitamente o no, tarde o temprano vas a tener que lidiar con la concurrencia en tu aplicación, es por eso que debes tener en mente que al utilizar threads existen ciertos riesgos que se deben considerar y evitar para tener un código thread-safe.

Errores

Podemos clasificar los errores al usar threads en tres diferentes tipos o enfoques:

  1. Security: Poder decir que un thread es seguro es algo complicado ya que si no tenemos suficiente sincronización, el orden de ejecución de las sentencias entre threads puede ser impredecible. Dado que múltiples operaciones entre threads pueden ser intercambiadas a la hora de ejecutarse por el runtime, es posible que en un punto del tiempo dos threads lean el mismo state y que uno de esos threads después modifique dicho state. Puede ser posible que el otro thread no refresque ese cambio en ese momento o puede ser que si. Es decir, el resultado no es determinístico si dichas condiciones se presentan. Para reducir este tipo de errores es recomendable aplicar synchronized en los bloques de código donde se modifique shared state.
  2. Liveness: Éste tipo de error puede ocurrir cuando un thread entra en un estado donde ya no puede continuar procesando su tarea. Por ejemplo, si un thread A espera el resultado de un thread B y dicho thread B nunca termina de procesar el recurso y regresar el resultado para que A lo procese, entonces A entrará en un estado de deadlock esperando infinitamente por el resultado de B. Este tipo de errores son difíciles de detectar porque solo ocurren en situaciones muy particulares y en lapsos de tiempo definidos, es decir, no siempre se manifiestan en fases de desarrollo o pruebas. Otro tipo de errores de tipo liveness son starvation y livelock.
  3. Performance: Son problemas relacionados a malos tiempos de respuesta, demasiado consumo de recursos, mala escalabilidad, entre otros. En general, al usar threads podemos incrementar cierto performance, pero todo tiene un costo y en este caso podríamos hablar de overhead en runtime. Por ejemplo, cuando un thread entra en modo de espera se hace un cambio de contexto para poder trabajar con otro thread. Dichos cambios entre contextos (suspenderlos, preservarlos y reactivarlos) implican consumo de CPU y reducen la escalabilidad. Cuando los threads comparten datos entre sí, requieren métodos de sincronización, estos métodos usualmente impiden que el compilador pueda optimizar llamadas, invalidación de caché, entre otros. Ésto a su vez genera un costo en performance.