La concurrencia es la capacidad de un programa de ordenador de ejecutar código en un orden diferente, es decir, que unas partes se ejecuten antes que otras, sin que afecte al resultado final.
Por lo general, en un lenguaje de programación imperativo, lo que nos enseñan a la hora de programar es que cuando se ejecuta un programa, las instrucciones se ejecutan aproximadamente de arriba a abajo. Esto no es del todo cierto, porque evidentemente hay instrucciones que puedes saltarte si no se entra en un if, o hay instrucciones que pueden ejecutarse varias veces dentro de un bucle; tampoco debemos olvidar las llamadas a funciones. Sin embargo, parece coherente que cuando tenemos una serie de instrucciones como,
indicarMenu();
leerOpcion();
Parece claro por lo que una persona recién llegada a programación, que en programación imperativa la llamada que aparece primero será la que antes se ejecute.
Con la concurrencia lo que haremos será organizar nuestro programa de tal forma que haya partes de un programa que no cumplan esto, y que de cara al flujo de ejecución del programa se lancen en un orden diferente al secuencial que se estudia en programación monotarea tradicional. La característica importante aquí es que el resultado final debe ser el mismo independientemente del orden en el que se ejecute el código.
Por ejemplo, si tienes un array de 10.000 elementos y tu objetivo es calcular los valores mínimo y máximo del mismo, te da igual si calculas primero el máximo y luego calculas el mínimo, que si primero calculas el mínimo y luego calculas el máximo. El resultado final deben ser los dos valores, pero qué función se llame primero es algo que importa poco a gran escala.
Y la concurrencia, en ese sentido, es una característica que se obtiene mediante el uso de una serie de técnicas de programación concretas, que exigen el uso de estructuras y construcciones nuevas que son precisamente las que vamos a estudiar en esta lista. Sin embargo, es probable que debamos adaptar la forma de estructurar nuestro programa para que encaje con estos principios. En caso contrario, no vamos a poder habilitarla fácilmente.
En el ejemplo de nuestro array y los máximos y mínimos, si usamos esas estructuras concurrentes para declarar hilos o tareas que calculen el máximo y el mínimo de un array, estaremos formalizando en código que son operaciones independientes y que el orden nos importa poco.
La relación que tiene esto con el paralelismo y la multitarea es que la concurrencia es lo que nos permite habilitar la programación paralela. Un programa se ejecuta en paralelo cuando en un instante de tiempo hay más de una unidad de proceso o CPU ejecutando instrucciones de un mismo programa. De este modo, un programa a cargo de realizar una tarea compleja, como puede ser convertir un archivo de vídeo, puede terminar en la menor cantidad de tiempo posible porque hay múltiples CPUs realizando parte de la misma.
Y la relación entre paralelismo y concurrencia es precisamente que una vez que nuestro programa está programado de forma concurrente, es posible asignar a cada una de estas CPUs cada una de esas zonas de código que pueden ejecutarse en orden independiente, para que precisamente cada CPU quede a cargo de una de las mismas. Puesto que el orden no importa, es lo ideal. Eso significa también que un programa que no sea concurrente no se va a poder ejecutar de forma exitosa de forma paralela precisamente porque no hay una manera clara de dividir el flujo de ejecución entre varias CPUs.
Finalmente, en el ejemplo del array, lo que ocurrirá si ejecutamos de forma paralela nuestro código ya concurrente, algo que perfectamente podría ocurrir es que las dos funciones se ejecuten a la vez. Una CPU calculará el máximo y otra CPU calculará el mínimo. Necesitaremos una estructura de control que se asegure de que la CPU que termina antes, espere a que termine la otra de hacer su cálculo, para poder entregar ambos resultados a la vez. Precisamente esta operación, esperar a que una tarea espere, también es otra técnica concurrente.
Incluso, es posible que tengamos un ordenador viejo que no tenga más de una CPU. Este ordenador no podría ejecutar código de forma paralela puesto que es imposible que haya más de una CPU haciendo cosas. Sin embargo, la concurrencia sí puede seguir siendo aplicada porque sólo afecta a la forma en la que estructuramos el programa.
Un ejemplo más complejo de cómo la concurrencia y el paralelismo puede afectar a una operación de larga duración. Si estamos procesando una imagen tomada con un telescopio que tiene un tamaño descomunal, y el objetivo es contar el número de estrellas que se ven en la imagen, ejecutar en secuencial todo un bucle que recorra cada píxel de la imagen en busca de estrellas no será lo más oportuno, porque entonces hemos entregado a una región de código todo el control sobre la tarea.
Sin embargo, si dividimos el procesamiento de la imagen en una cuadrícula de regiones, y convertimos el problema de procesar una imagen en procesar un sector de la imagen, ahora tenemos N problemas menores que son independientes entre sí, porque calcular un sector no depende de otro sector. Con este cambio, ahora tenemos una estrategia de procesado concurrente donde podríamos tener múltiples CPUs procesando cada uno de los sectores de la imagen en paralelo. Simplemente, a medida que vayan terminando de procesarse los sectores se van a ir generando unos números que finalmente habrá que sumar.
Esta misma estrategia nos vale para otras cosas como por ejemplo el procesamiento de vídeo o renderizar una animación 3D, que son casos reales donde, de hecho, la concurrencia, el paralelismo y el uso de múltiples hilos y CPUs facilita que una tarea que de otro modo llevaría horas o días tome menos tiempo.