Hace muchos años, la programación multihilo solo servía para darnos a los usuarios la sensación de multitarea y que el ordenador pudiera respondeer cuando interactuábamos con él. Normalmente sólo teníamos un único núcleo de CPU y solo podía ejecutar una cosa en un momento del tiempo determinado. Aunque el sistema operativo se encargaba de ir cambiando las tareas que iba ejecutando varias veces por segundo, y por eso podíamos estar viendo una página web y al mismo tiempo chateando con otro programa, copiando un archivo, etc. Esta multitarea también nos ayudaba mucho cuando una aplicación estaba esperando un dato del disco o de un dispositivo (ratón, teclado, red...), ya que estos dispositivos son mucho más lentos que la CPU del ordenador. Es decir, mientras la CPU puede procesar millones de operaciones por segundo, pedir un dato a un disco duro, puede tardar lo mismo que tardarían varios miles de operaciones, y si el dato viene de Internet, puede tardar mucho más. Así que los sistemas operativos multitarea, suspenden la ejecución de dicha aplicación hasta que el dato esté listo, y así aprovechan los ciclos de procesador que podían haberse perdido para hacer cosas más productivas como la descompresión de fotogramas de un vídeo de gatitos.

Eso sí, si en una CPU monohilo ejecutamos dos tareas que requieren CPU de forma intensiva como la compresión de vídeo, renderizado 3D o aplicación de filtros sobre imágenes, nos daremos cuenta de que el tiempo total de la ejecución de las tres tareas es aproximadamente el mismo que ejecutar una tarea detrás de otra.

Pero en los tiempos que corren, es muy común encontrar procesadores con varios núcleos, incluso con varios hilos por núcleo. Al tener varios núcleos, podremos ejecutar varios programas y normalmente el sistema operativo se encarga de colocarlos en uno u otro núcleo, de forma que, ya que nuestro procesador puede ejecutar varias cosas a la vez, aprovechemos esa potencia para terminar las tareas antes.
Aunque ahora el problema suele ser que las aplicaciones no están pensadas para ejecutarse en varios hilos. Bueno, actualmente muchas aplicaciones pueden aprovechar la potencia de los procesadores modernos, como compresores de vídeo, software de minería de criptomonedas, tratamiento de imágenes etc. Pero, muchos programas comunes de búsqueda de datos en ficheros, compresión/descompresión, y muchos de nuestros scripts en Bash o Python no están preparados. 


Ejecución de tareas en paralelo en Bash

Cuando, en Bash queremos ejecutar dos tareas a la vez, siempre podemos ejecutarlo con un & al final, volveremos a nuestro shell para ejecutar más cosas mientras el programa inicial se está ejecutando:

$ programa &
19238
$ … ejecuto más cosas …


O también podemos ejecutarlo normalmente, pulsar Control+Z (^Z). El programa se detendrá, si escribimos en la consola "fg" el programa seguirá en primer plano (foreground), pero si ponemos "bg" el programa seguirá en segundo plano (background), y mientras éste está corriendo, podremos ejecutar más cosas:

$ programa
…
^Z
[1]+ Stopped   programa
… ejecuto más cosas …
$ fg
programa
… programa sigue ejecutándose …
^Z
[1]+ Stopped    programa
$ bg
[1]+ programa &
… puedo ejecutar lo que quiera …


GNU Parallel

Con esto puede ser suficiente para muchos de nosotros pero, ¿y si queremos utilizar más hilos? Es un poco pesado ejecutar varios programas con sus argumentos y una & al final. ¿O queremos que los argumentos vayan cambiando según la ejecución? ¿O incluso queremos ejecutar cientos de tareas pero que se vayan repartiendo la CPU de 2 en 2, de 4 en 4 o más dependiendo del número de núcleos que tengamos? Para eso, y mucho más, tenemos la utilidad GNU Parallel.
Un ejemplo típico es crear un programa que calcule dígitos del número PI con un hilo de CPU, es muy fácil con bc:

#!/bin/bash
echo "scale=4000; a(1)*4" | bc -l

Lo salvamos como pi.sh y le damos permiso de ejecución con:

$ chmod +x pi.sh


Este programa, ejecutado en mi ordenador tarda unos 11 segundos en terminar. Suponiendo que nuestro equipo tenga 2 núcleos, podemos hacer:

$ time ./pi.sh & time ./pi.sh


Veremos que tarda también 11 segundos (más o menos). Esto lo podemos hacer con GNU Parallel de esta forma:

$ time seq 2 | parallel -n0 ./pi.sh

Donde se ejecutará el comando pi.sh dos veces y de forma simultánea. Pero, aunque esta orden pueda parecer complicada, podemos jugar un poco con ella, para ejecutar pi 4 veces de forma simultánea:

$ time seq 4 | parallel -n0 ./pi.sh

Si nuestra CPU tiene 4 núcleos, seguirá tardando 11 segundos, en realizar cuatro tareas, pero si nuestra CPU tiene 2 núcleos, tardará más de 22 segundos. Ya que las tareas utilizan mucho la CPU y es un trabajo extra para el sistema operativo intercalarlas para que se ejecuten a la vez. Pero, imaginémonos que queremos ejecutar 4 veces pi, pero que se repartan de 2 en 2, para ello podemos hacer:

$ time seq 4 | parallel -n0 -j2 ./pi.sh

Si queremos, podemos ejecutar un gestor de tareas o el comando "top" con el que veremos qué tareas están en ejecución cada vez.


Varias tareas con argumentos

Ahora vamos a modificar pi.sh así:

#!/bin/bash
DECIMALES=$1
echo "scale=$1; a(1)*4" | bc -l

De modo que tengamos que introducir el número de dígitos que queremos de Pi. Así ejecutaremos pi.sh de forma concurrente pero con parámetros diferentes en cada ejecución. Podemos jacer lo siguiente:

$ time echo "1000 2000 3000 4000" | tr ' ' '\n' | parallel ./pi.sh

O si queremos que solo haya dos ejecuciones de pi.sh simultáneas:

$ time echo "1000 2000 3000 4000" | tr ' ' '\n' | parallel -j2 ./pi.sh

Nota: El uso de tr es porque parallel necesita que los argumentos vayan uno por línea. Si los argumentos los tenemos en un archivo y uno por línea podríamos hacer:

$ cat argumentos | parallel -j2 ./pi.sh


Combinaciones de argumentos

Con GNU Parallel podemos incluso combinar argumentos. En este ejemplo, cambiamos de base varios números binarios a base 10

$ parallel echo -n {1}{2}{3}{4} = \;'echo "ibase=2;{1}{2}{3}{4}" | bc' ::: 0 1 ::: 0 1 ::: 0 1 ::: 0 1
0000 =0
0001 =1
0010 =2
0011 =3
0100 =4
0101 =5
0110 =6
0111 =7
1000 =8
1001 =9
1010 =10
1011 =11
1100 =12
1101 =13
1110 =14
1111 =15

Y como vemos, GNU Parallel, además de proporcionarnos una forma sencilla de ejecutar programas simultáneamente, nos proporciona también una forma de combinar argumentos y generar resultados muy potente. Utilizando {1}, {2},...{n} dependiendo del número de argumento que estemos tratando

Convirtiendo masivamente archivos de JPG a PNG

Uno de mis usos preferidos, es la conversión automática de archivos, o tratamiento de imágenes con ImageMagick. Además, podemos automatizar esta conversión para realizarla en cientos de archivos y despreocuparnos del ordenador ya que él se encarga de hacer el trabajo. Una tarea muy sencilla es convertir un archivo de JPG a PNG con convert. Pero, ¿si tenemos una carpeta llena de archivos? Podemos ejecutar convert en todos ellos, y hacerlo uno detrás de otro, pero podríamos hacerlo, por ejemplo de 2 en 2 o de 4 en 4 dependiendo dee nuestra CPU, incluso podríamos subir a 3 o 5 porque la conversión de archivos implica lecturas y escrituras y es tiempo que podemos estar procesando datos (los archivos convertidos entrarán en el directorio convertidas):

$ find -maxdepth 1 -name*.jpg’ | parallel --progress -j3 convert -verbose {} convertidas/{/.}.png

Fijaos cómo en el segundo argumento le quito la extensión para llamarla png. Podría variar el número del argumento -j según mis preferencias. Además, he introducido --progress que nos indicará el progreso de la tarea.
Como último ejemplo, nos basaremos en el anterior. Pero esta vez, utilizaremos zenity para presentar una barra de progreso de manera gráfica. ¡Porque, que sea terminal no significa que no sea visual!

$ find -maxdepth 1 -name*.jpg’ | parallel --bar convert {} convertidas/{/.}.png 2> >(zenity --progress --auto-kill)

¡Espero que les haya resultado útil!

¡Atención! Este sitio usa cookies y tecnologías similares.

Si no cambia la configuración de su navegador, usted acepta su uso. Saber más

Acepto

Vea nuestra política de cookies y enlaces de interés aquí