Cómo lidiar con la concurrencia sin morir en el intento

Algunas veces nos hemos encontrado con el problema de que ciertos procesos requieren un tiempo considerable para realizar diversas tareas. La solución a este problema parece bastante sencilla, se levantan más instancias del mismo proceso, y listo… ¿o no? Obviando el típico ejemplo de que nueve mujeres no pueden traer al mundo juntas un bebé en un mes, expondré un ejemplo más cercano sobre los problemas de la concurrencia.

Supongamos que tengo un programa que es capaz de escribir posts. Y lo único que tengo que hacer es indicarle el tema sobre el que quiero que escriba. Resulta que a mí se me ocurren temas más rápidamente de lo que éste redacta, por lo que tengo una lista con todos los temas. Ahora decido levantar dos instancias del proceso para que escriban concurrentemente. Si en mi lista tengo los temas «A», «B» y «C», y levanto ambos a la vez, leerán a la vez la lista y ambos escribirán sobre el tema A. De esta forma, mis procesos consumen más recursos, pero no me arreglan nada.

Pero podría ser todavía peor. Supongamos que, por algún milagro, consigo que el Proceso 1 escriba sobre el tema A, y el 2 sobre el tema B. Pero ambos deben escribir los escrito en un fichero, añadiéndolo al final. Si terminan a la vez, ambos abrirán el fichero a la vez (por lo que lo del otro no estará) y escribirán sus datos. Por lo tanto, perderé uno de los dos posts… Pérdida de datos, esto ya duele, ¿verdad?

Solución a la concurrencia: Exclusión mutua

El 1 de Julio, parte del equipo de desarrollo de Irontec tuvimos la suerte de asistir a la charla de César Suárez, en la cual habló sobre Symfony y Concurrencia El Componente Lock.  En ésta presentó como poder sortear el problema de la concurrencia con Symfony (las diapositivas de la misma se encuentran aquí) empleando algoritmos de exclusión mutua.

El primer paso es identificar la sección crítica del código. En el ejemplo expuesto, ésta sería tanto la lectura como la escritura. Para simplificar el ejemplo, quedémonos solo con la lectura de la lista de temas. El objetivo será que los procesos no lean de la lista a la vez, y que borren el tema sobre el cual van a escribir. La solución que nos presentó César es que el código de la sección crítica se bloquee cuando un proceso accede al mismo. De esta forma, ningún otro podrá acceder hasta que se libere. Como una imagen vale más que mil palabras, en el siguiente esquema de las diapositivas de César Suárez se pueden ver los estados por los que pasará la sección crítica cuando la adquiramos y la liberemos.

Estados de la sección crítica

Extracto de las diapositivas de César Suárez

Exclusión mutua con Symfony 3.4

Como no podía ser de otra forma, en las conferencias deSymfony César explicó como solucionar este problema utilizando el framework Symfony, concretamente la versión 3.4 del mismo. Y para ello, recurriremos al componente Lock de dicho framework. Este es una evolución del actual LockHandler, y está pensado principalmente para comandos. Será tan sencillo como crear un Lock, realizar un bloqueo justo antes de ejecutar la sección crítica, y liberarlo después.

Para realizar el bloqueo, tenemos dos opciones:

  • $lock->acquire(true)  : es bloqueante. Por lo tanto, si ya hay algún proceso ejecutando la sección crítica, el proceso actual no seguirá hasta que esta se libere.
  • $lock->acquire()  : no es bloqueante, así que, después de esto, necesitaremos ejecutar $lock->isAcquired()  para saber si tenemos acceso a la sección crítica o si ya había un proceso en ella.

Para liberar el lock, habría que hacer algo así:

if ($lock->acquire()) {
  try{
    //sección crítica
  } finally {
    $lock->release();
  }
}

Inconvenientes de la Exclusión mutua

La exclusión mutua tiene un problema principal significativo: ¿Qué pasa si mi Proceso 1 bloquea la sección crítica y, por lo que sea, muere antes de liberarla? Ésta se quedaría bloqueada y el Proceso 2 (ni ningún otro, si lo hubiera) no podría acceder a esta parte del código. La solución a esto pasa por crear bloques con un tiempo de expiración. Nuevamente, esto presenta otro dilema. ¿Y si se queda atascado (pero no muerto) en algún punto, y el tiempo de expiración llega antes de que el Proceso 1 finalice? En este caso, tendríamos dos procesos en la temida sección crítica. Ante esta posibilidad, existe la opción de refrescar el tiempo de expiración a lo largo de la sección crítica.

Aún así, nunca podremos prever todas las posibilidades, por lo tanto, en mi opinión, es mejor fijar un tiempo de expiración relativamente alto, aunque esto implique que el resto de procesos estén un tiempo ociosos. Finalmente, considero que la opción de que dos procesos estén a la vez en la sección crítica es preferible a tener una caída por un bloqueo no controlado de la sección crítica. Pero, por supuesto, symfony siempre da la opción de elegir.

Solución de Irontec

Desde Irontec, nos hemos tenido que enfrentar al problema de la concurrencia. Debido a esto, hemos ideado una solución alternativa al Lock, para evitar los problemas planteados por la Exclusión mutua. Retomando el ejemplo planteado al principio, dividiremos la funcionalidad en dos partes.

Por un lado,  necesitamos obtener de forma secuencial los temas de la lista. Para ello, crearemos un proceso, y solo uno, que lea estos temas. A continuación, levantaremos n instancias de otro proceso que, dado un tema, redacten un post. El primer proceso enviará a cada una de las demás instancias el tema sobre el que deben escribir. De esta forma, conseguimos que para los procesos que redactan los posts, la fuente de los temas sea transparente. Así mismo, evitamos que dos escriban sobre un mismo tema, ya que hay un único proceso con acceso a la lista. Finalmente, no habrá problemas de que parte del código se quede bloqueado, ya que no utilizamos bloqueos. Y no necesitamos preocuparnos de que dos procesos entren a la vez en la sección crítica, ya que hay un único proceso con esta.

Obviamente, esto nos sirve únicamente para lectura. Si volvemos al primer ejemplo, seguiríamos con el problema de la escritura, ya que serían las n instancias las que deberían escribir. Por lo tanto, en este caso, recurriríamos a la brillante solución expuesta por César Suárez.

Conclusión

El componente Lock viene pisando fuerte, y promete evitarnos bastantes dolores de cabeza. Este post es únicamente la punta del iceberg, y lo expuesto aquí es una pequeña parte de las posiblidades de este. Para ampliar información, recomiendo encarecidamente la lectura de las diapositivas de César Suárez. En ella expone información sobre los Stores que podemos utilizar para gestionar los bloqueos, y muchísimas cosas más. También se puede recurrir a la documentación oficial del componente Lock.

Y, para todos aquellos que trabajen con symfony, las conferencias deSymfony siempre superan nuestras expectativas. Sobre todo con charlas como Symfony y Concurrencia El Componente Lock.



¿Te gusta este post? Es solo un ejemplo de cómo podemos ayudar a tu empresa...
Sobre Natalia Aznar

Desarrolladora backend

Queremos tu opinión :)