La programación concurrente y sus peligros: Parte 1

La programación concurrente y sus peligros: Parte 1

1000 667 Carlos Gomez Pintado

Si trabajas en el área de la programación es seguro que has escuchado hablar del término “Programación concurrente. Esa forma de programar, que nos permite dividir los procesos o funciones en diferentes “Threads” para agilizar la ejecución de nuestros programas y acelerar sus algoritmos. Así expuesto suena como algo fantástico, cuyo uso solo traería beneficios, y por tanto, todo el mundo debería aprender esta técnica para ponerla en uso en su día a día, pero… ¿Realmente es segura?

La programación concurrente es uno de los tipos de programación más demandados por su utilidad y múltiples ventajas, sin embargo, como profesionales hay que ser muy cuidadosos a la hora de programar y, tener en cuenta, cómo después, nuestro ordenador interpretará esas órdenes y las ejecutará. Pongamos un ejemplo:

Imaginemos el siguiente escenario: Estamos construyendo una casa, y hemos calculado que tenemos que utilizar un total de 10.000 ladrillos, sabiendo que somos capaces de colocar 500 ladrillos cada día, calculamos que en 20 días hemos terminado nuestra casa. El coste diario de construcción, es de 100€, por lo que colocar estos ladrillos nos costará un total de 2.000€.
Ahora llega el jefe de la obra, y nos dice que 20 días es inaceptable, que necesitan que esté lista en 10 días, y para ello contrata un trabajador más, que al igual que el primero cobrará 100€ al día y es capaz de poner 500 ladrillos.
Entonces tenemos 2 trabajadores a la vez (trabajando de forma concurrente) que colocarán 1.000 ladrillos diarios a un coste de 200€/día, por lo que en 10 días tendremos lista la casa, a un coste de 2.000€.

Algo similar ocurre en la programación concurrente, si tenemos que realizar una función, por ejemplo, una búsqueda en profundidad en un árbol binario, un solo Thread tendría que recorrerse el árbol por completo y reportar los resultados, pero si somos capaces de dividir este árbol en dos, por ejemplo tomando el nodo inicial como referencia, y asignando la rama derecha a un Thread y la rama izquierda al otro, obtenemos dos Threads trabajando a la vez para su recorrido, por lo que el tiempo de ejecución se verá reducido. Si somos capaces de replicar esta técnica con varios Threads, la mejora sería más que notable.

Volvamos ahora al ejemplo de nuestra casa:

Imaginemos ahora, que tan solo contamos con una pala, necesaria para poder colocar nuestros ladrillos, y que ambos obreros la necesitan para poder trabajar. Esto hará que uno de los trabajadores esté ocioso, provocando que el rendimiento ya no sea de un 100% debido a los tiempos de espera. En este escenario ya no alcanzarán 10 días para completar el proyecto, si no que el tiempo aumentaría a 15 días, ya que el recurso compartido de la pala, está ocupado la mitad del tiempo disponible. En este escenario nuestro presupuesto acaba de subir a 3.000€.

Si traducimos este caso a la programación, puede ocurrirnos lo mismo cuando varios Threads traten de acceder a un recurso compartido, como por ejemplo puede ser una variable o un objeto, que obligaría a uno o varios de nuestros procesos a estar a la espera de que este recurso se libere para poder hacerse con el control de él.
Cuando tenemos tan solo dos o tres Threads concurrentemente trabajando, es fácil que todos puedan acceder antes o después a este recurso, pero… volviendo a nuestro ejemplo anterior ¿Qué pasaría si en vez de dos obreros tuviésemos, por ejemplo 100?

Anteriormente, hemos visto que con dos obreros y una sola pala, nuestro rendimiento se reduce en un 50%, pero ahora mismo, aumentando el número de trabajadores a 100, esta disminución es mucho mayor y dejaría de ser lineal. El coste de mantenimiento incrementaría notablemente y, probablemente, se generaría alguna ruptura en el uso del recurso compartido ya que todos querrían acceder al mismo tiempo. Adicionalmente, al no haber nadie regulando el uso del recurso, se podría dar el caso de obreros inactivos por tiempo completo, incapaces de realizar su trabajo y entrando en un bucle de inactividad.

A estas alturas, ya nos hemos topado con dos problemáticas, un uso de recursos compartidos ineficiente, ya que no todos podrían utilizarlo a la vez, y una posible espera continua, provocando procesos que nunca finalicen.
Es por ello que, hay que ser muy precavidos al decidir en qué tipo de estructuras se utiliza la concurrencia, ya que en muchos escenarios podría tener el efecto contrario al buscado.
Adicionalmente hay que establecer estructuras de control, para administrar el acceso a los recursos compartidos y así evitar esperas innecesarias.

En futuras ediciones, profundizaremos más en los riesgos de la programación concurrente, y en las posibles soluciones que se han implementado para solventarlos.

Hack && fun 😉