Orden y concierto

Programar es controlar. Ejercer de comandante de una máquina cabezota dándole un fin y los medios (en forma de instrucciones) para alcanzarlo. Ponemos orden en los pensamientos de esas entrañables calculadoras XXL (y en no pocas ocasiones XXX) y las empujamos a trabajar en nuestro mejor interés. Ahora lo llaman arte o artesanía, antes nos conformábamos con considerarlo ingeniería. No hay mucha magia detrás del asunto: se les habla en un lenguaje que entiendan, y suelen obedecer. Escribir software consiste en traducir una serie de instrucciones (un algoritmo) a un lenguaje de programación, para su posterior transformación en código ejecutable por la máquina. Dead simple, right? Pues a veces sí, pero a veces no.

Si el algoritmo que estamos aplicando es trivial, y se limita a una serie de pasos dados en un orden estricto, la traducción a código ha de ser igualmente directa. Pero, ay amigos, el software tiende a modelar problemas de la vida real, y en la vida real raramente son tan sencillas las cosas. Una estrategia estándar para atacar un problema complejo en descomponerlo en problemas más pequeños, fáciles (o más fáciles al menos) de solventar. La combinación de todas esas soluciones es la solución al problema original. Y de eso quiero charlar un rato hoy, de la combinación de procesos en la resolución de problemas complejos.

Presentando Ducksboard en eventos y reuniones durante el último año y medio me he llenado la boca de palabros como asincronía, concurrencia, paralelismo, código no-bloqueante, … dando por hecho que todo asistente a las charlas conocía bien esos conceptos. Craso error. Que un tema forme parte de tu día a día durante años no lo hace ni más sencillo ni más conocido por el gran público. Así no es de extrañar que en varias ocasiones, tras la charla, se me acercase alguien a preguntar por esos conceptos, con los que no estaba familiarizado. Y es que al fin y al cabo no son técnicas triviales ni demasiado bien comprendidas. Porque, aunque nos suenen y sepamos de qué van, ¿cuál es la diferencia entre concurrencia y paralelismo? ¿Son lo mismo el código asíncrono y el no-bloqueante? Así que aquí estoy para enmendar mi error y dar el contexto que debí dar entonces.

Empecemos por la concurrencia, que es el concepto más trillado de todos ellos. Se aplica en múltiples ámbitos, aún sin salir del software, pero vamos a centrarnos en el diseño de aplicaciones (y no en conexiones concurrentes en una aplicación de red, por ejemplo). La palabra es bastante explicativa en sí misma, denota el hacer varias cosas a la vez, ¿verdad? ¡Pues no! Bueno, en parte, he ahí la confusión. Una solución concurrente es aquella en que varios componentes independientes colaboran para resolver el problema. Así que el “varias cosas” sigue ahí, pero el “a la vez” no tiene por qué. La simultaneidad no es una característica necesaria en los sistemas concurrentes. Pero… si los componentes están colaborando, sí se están ejecutando a la vez, ¿no? Pues no siempre. Y aquí toca introducir la diferencia entre concurrencia y paralelismo.

El paralelismo es la ejecución simultánea (y aquí sí, estrictamente simultánea) de varios procesos. Ya sea en una misma máquina o en varias. Para tener paralelismo en una única máquina la misma habrá de tener varias CPUs (o núcleos), claro está, de otro modo es imposible ejecutar múltiples instrucciones a la vez. Volvamos a la distinción concurrencia/paralelismo, y a la pregunta “¿es todo sistema concurrente inherentemente paralelo?”. Un ejemplo para aclararlo: vuestro sistema operativo. Windows, MacOS, Linux, da igual. Podéis usar el mouse, el teclado, oir música, cargar una página web, todo a la vez. Incluso en máquinas de una única CPU. ¿Cómo es posible? Pues porque en realidad no se ejecutan a la vez. El scheduler del sistema operativo va repartiendo la CPU disponible entre los procesos que la necesitan, que se van turnando para poder operar. No corren simultaneamente, pero la combinación de sus ejecuciones resuelven las necesidades multitarea del sistema operativo. Tenemos concurrencia, pero no paralelismo.

Y esa es la diferencia entre ambos. Parafraseando al gran Rob Pike (su charla -aquí slides– sobre esta diferencia es en gran parte motivadora de este post), la concurrencia va de lidiar a la vez con varias cosas, y el paralelismo de hacer varias cosas a la vez. Un sistema operativo lidia con muchas cosas a la vez, pero no tienen por qué ocurrir simultaneamente, aunque el sistema las tenga todas controladas. En un sistema paralelo, todas esas cosas pasan simultaneamente de verdad. La concurrencia es una característica del diseño de la aplicación o el sistema, no tanto de su implementación. El paralelismo es una característica de su ejecución. Un sistema o aplicación puede ser concurrente y paralelo a la vez, claro, pero uno no implica que se cumpla el otro. Y hasta aquí con la concurrencia y el paralelismo.

Y vamos ahora a por la asincronía, el bloqueo o no bloqueo y demás hierbas. Son conceptos vinculados a la entrada/salida (I/O a partir de ahora, por salud mental y vagancia) de las aplicaciones. Ya sabéis, la comunicación de red, acceso al disco y todo lo que implique ir a buscar datos lejos de la RAM dónde poco puede hacer la CPU. Muy en voga hoy en día, con aplicaciones de red dominando el panorama y clientes y servidores floreciendo por todas partes. Sistemas distribuidos en que múltiples componentes en distintas máquinas colaboran. Empecemos por el principio: ¿que problema trata de resolverse? Pues son dos. El infra-aprovechamiento de recursos (principalmente CPU) y el bajo rendimiento de la aplicación. Veamos por qué. Pongamos un ejemplo clásico: el envío de e-mails desde una aplicación.

Tenemos nuestra aplicación que hace lo que quiera que haga, pero además envía mails informativos a usuarios por la razón que sea, pongamos reporting. En el estilo de programación de toda la vida (síncrono), obtendríamos una lista de direcciones a las que enviar mails, y en un bucle llamarías a nuestra función send_email(address). ¿Qué pasaría entonces? Que en cada llamada a send_email: esperaríamos a que se abriera la conexión con el servidor SMTP, se negociasen autenticación y parámetros de la sesión, se enviaran los bytes del mensaje en sí, el servidor confirmase el envío, y se cerrara la conexión. A lo tonto, un par de segundos si la red es lentilla. Si tenemos 1000 usuarios a los que enviar un mail, hablamos de 2000 segundos enviando mails. Casi nada. ¿Y qué hace nuestra aplicación durante esos 2000 segundos? Pues nada más que esperar. Ahí, parada, con la CPU muerta de la risa.

Tenemos un problema de bloqueo: la espera de I/O bloquea toda la aplicación, y desperdiciamos la CPU durante todo el proceso. Además la aplicación es lenta, tardamos 2000 segundos en enviar 1000 mails, no es aceptable. ¿Soluciones? Varias. De hecho expliqué unas pocas aquí. ¿Cómo resuelve el código asíncrono el problema? Pues no esperando a que la I/O complete para seguir ejecutando nuevas instrucciones. De hecho la idea tras el código asíncrono es “voy a hacer I/O, que espero sea lenta, así que me olvido de ella mientras hace su parte y sigo ejecutando código, y cuando haya terminado, sólo entonces, vuelvo para comprobar el resultado”. De modo que no bloqueamos durante la I/O y la ejecución de la aplicación continúa su camino, dándole buen uso a esa CPU de otro modo desaprovechada. De nuevo la duda: ¿son entonces asincronía y código no-bloqueante equivalentes?

Pues… no del todo, pero es un tema sutil. Para explicarlo, un poco más de detalle sobre cómo funciona la asincronía. La idea es “olvidarse” temporalmente de la I/O, y volver a hacerle caso cuando haya acabado. ¿Cómo sabemos que ha terminado? Pues hay varios mecanismos, los principales: eventos y callbacks. Ambos funcionan de manera muy parecida. La aplicación colabora con el sistema operativo para que este último avise a la primera cuando las operaciones de I/O hayan concluido. El sistema operativo se encarga de la I/O, así que él sabe cuando acaban esas operaciones. En ese momento emite un evento, que la aplicación captura, o llama a la función de callback que la aplicación ha registrado. En ambos casos la aplicación pasa a un modo pasivo, espera que se le notifiquen los cambios en vez de preguntar por ellos. De ahí el apelativo de asíncrono: al no esperar a que se completen las operaciones de I/O, no sabemos en que orden llegarán esos resultados. El orden en que van a ejecutarse nuestras instrucciones pasa a ser desconocido a priori. En el código síncrono sabemos en que orden se ejecutan porque esperamos a que completen antes de pasar a la siguiente.

La idea original de socket no-bloqueante no incluye toda la magia de las notificaciones, es mucho más simple: simplemente hay que retornar inmediatamente de las peticiones de I/O, lo que se haga luego es indiferente. Sería igualmente válido que la aplicación, en vez de esperar notificaciones, comprobase si la operación de I/O ha terminado consultado el estado del socket abierto (la representación en el sistema de la conexión remota). Si el socket sigue ocupado, la aplicación simplemente retorna algún código de error y continúa con su ejecución, para volver a comprobar más tarde el estado del socket. En definitiva, la aplicación no bloquea, pero tampoco se olvida de la I/O, la comprobación del estado de la misma se hace de manera activa, a diferencia del modelo de eventos y callbacks (estamos haciendo polling). Sigue siendo una aplicación no-bloqueante en I/O, pero no asíncrona. En definitiva, las I/O asíncronas son un subconjunto de las I/O no-bloqueantes, pero no al revés.

Y ahí queda eso. Espero que este baile de conceptos le pueda ser de utilidad a alguien. Son conceptos complejos y en eterno debate (es fácil encontrar literatura que defienda definiciones diferentes a las que he dado aquí), pero creo que lo aquí expuesto captura razonablemente bien las ideas principalmente aceptadas. En este mundo de aplicaciones de red para millones de usuarios simultáneos estos conceptos y técnicas son cada día más cruciales, ¡así que estad seguros de tener un mapa mental claro del asunto!

Dudas, correcciones, críticas o flamewars entre node.js y otros en los comentarios o a mi siempre entretenida cuenta de Twitter, @aitorciki. ¡Hasta pronto!

Advertisements

6 thoughts on “Orden y concierto

    1. Lo malo es lo de “.js”, por lo demás la idea no es mala del todo (lo de los callbacks inlineados acaba enguarrando un poco) ;)

  1. El ejemplo que yo use la última vez que explique esto fue el del circo donde un malabarista hace girar platos sobre varillas.

    Para mantener los platos girando hay que dedicarle muy poco tiempo a cada varilla y hay que ir de una a otra (luego veremos que no). Concurrència en toda regla.

    Nótese que el orden lo marca la velocidad de giro de los platillos. Esto lo digo por que que platillo girar no es lo importante, sino que se tenga que ir de un platillo a otro para mantenerlos todos encima de sus varillas girando.

    Para el paralelismo pues muy fácil. Como el malabarista tiene dos manos puede acelerar dos platillos a la vez.

    Ea, explicación más simple imposible… A no ser que lo tengas que explicar en inglés :-)

      1. Vale, te dejo un link donde se demuestra que soy un aprendiz del paralelismo…

        No hay concurrencia, por eso :)

  2. Me ha encantado el post. Muy claro y bien explicado. Enhorabuena, me lo quedo como post de referencia. Te sugiero, si es posible, que le pongas alguna etiqueta que indique mejor la temática para que sea más fácil localizarla en el futuro.

    Ladrillaco de lo más útil ;-)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s