Una conversión/reemplazo universal "mientras se lee algo" a "paralelo"

Una conversión/reemplazo universal "mientras se lee algo" a "paralelo"

Tengo un montón de scripts que hacen esto:

command | while read something; do
    a
    long
    list
    of
    commands
done

¿Alguien ha pensado alguna vez en cómo ejecutar todos los comandos que se envían a través de la tubería parallel? Hayad hocsoluciones a este problema pero estoy buscando algogeneralque requiere sólo cambios mínimos en mis scripts y, si es posible, también permite ejecutarse incluso cuando parallelno está instalado.

commandLo anterior puede ser prácticamente todo, sin limitarse a un solo comando. a long list of commandstambién puede ser absolutamente diferente.

Por ejemplo, considere esta línea que cambia las fechas de modificación de los archivos extraídos en un repositorio git a la fecha en que se cambiaron por última vez:

git ls-tree -r --name-only HEAD | 
while read filename; do
   unixtime=$(git log -1 --format="%at" -- "${filename}");
   touchtime=$(date -d @$unixtime +'%Y%m%d%H%M.%S');
   touch -t ${touchtime} "${filename}";
done

De forma predeterminada, es terriblemente lento porque git logambos touchson comandos bastante lentos. Pero es sólo un ejemplo y, además, sencillo.

Respuesta1

Usaría una función bash y la llamaría:

myfunc() {
   filename="$1"
   unixtime=$(git log -1 --format="%at" -- "${filename}");
   touchtime=$(date -d @$unixtime +'%Y%m%d%H%M.%S');
   touch -t ${touchtime} "${filename}";
}
export -f myfunc

git ls-tree -r --name-only HEAD | parallel myfunc

Úselo parallel -0si desea dividir en NUL.

Si desea ejecutar lo anterior sin instalar GNU Parallel, puede usar:

parallel --embed > myscript.sh

y luego agregue lo anterior a myscript.sh.

Respuesta2

Es posible hacer que bash o ksh ejecuten una lista de comandos independientes simultáneamente, de modo que cada secuencia comience un nuevo comando tan pronto como finalice su tarea anterior. Las transmisiones se mantienen ocupadas excepto los comandos finales.

El método básico es iniciar una serie de shells asincrónicos que leen desde el mismo canal: el canal garantiza el almacenamiento en búfer de línea y lecturas atómicas (se puede usar un archivo de comandos con redirección, cat file |pero no mediante redirección).

Los comandos pueden ser cualquier shell de una sola línea (en la sintaxis correcta para el shell propietario de la secuencia), pero no pueden depender de los resultados de comandos anteriores, ya que la asignación de comandos a las secuencias es arbitraria. Los comandos complejos se configuran mejor como scripts externos para que puedan invocarse como un comando simple con argumentos.

Esta es una prueba de seis trabajos en tres corrientes, que ilustra la superposición de trabajos. (También probé 240 trabajos en 80 secuencias en mi computadora portátil).

Time now 23:53:47.328735254
Sleep until 00 seconds to make debug easier.
Starting 3 Streams
23:54:00.040   Shell   1 Job   1 Go    sleep 5
23:54:00.237   Shell   2 Job   2 Go    sleep 13
23:54:00.440   Shell   3 Job   3 Go    sleep 14
Started all Streams
23:54:05.048   Shell   1 Job   1   End sleep 5
23:54:05.059   Shell   1 Job   4 Go    sleep 3
23:54:08.069   Shell   1 Job   4   End sleep 3
23:54:08.080   Shell   1 Job   5 Go    sleep 13
23:54:13.245   Shell   2 Job   2   End sleep 13
23:54:13.255   Shell   2 Job   6 Go    sleep 3
23:54:14.449   Shell   3 Job   3   End sleep 14
23:54:16.264   Shell   2 Job   6   End sleep 3
23:54:21.089   Shell   1 Job   5   End sleep 13
All Streams Ended

Este es el script proxy que proporciona la depuración para esos trabajos.

#! /bin/bash

#.. jobProxy.
#.. arg 1: Job number.
#.. arg 2: Sleep time.
#.. idStream: Exported into the Stream's shell.

    fmt='%.12s   Shell %3d Job %3d %s sleep %s\n'
    printf "${fmt}" $( date '+%T.%N' ) "${idStream}" "${1}" "Go   " "${2}"
    sleep "${2}"
    printf "${fmt}" $( date '+%T.%N' ) "${idStream}" "${1}" "  End" "${2}"

Este es el script de gestión de transmisiones. Crea los comandos de trabajo para ejecutar los servidores proxy e inicia los shells en segundo plano.

#! /bin/bash

makeJobs () {

    typeset nJobs="${1}"

    typeset Awk='
BEGIN { srand( Seed % 10000000); fmt = "./jobProxy %s %3d\n"; }
{ printf (fmt, $1, 2 + int (14 * rand())); }
'
    seq 1 "${nJobs}" | awk -v Seed=$( date "+%N$$" ) "${Awk}"
}

runStreams () {

    typeset n nStreams="${1}"

    echo "Starting ${nStreams} Streams"
    for (( n = 1; n <= nStreams; ++n )); do
        idStream="${n}" bash -s &
        sleep 0.20
    done
    echo "Started all Streams"

    wait
    echo "All Streams Ended"
}

## Script Body Starts Here.

    date '+Time now %T.%N'
    echo 'Sleep until 00 seconds to make debug easier.'
    sleep $( date '+%S.%N' | awk '{ print 60 - $1; }' )

    makeJobs 6 | runStreams 3

Respuesta3

En lugar de ejecutar git ls-treey luego git log, datey touchvarias veces en un ciclo bash mientras se lee, el siguiente script en Perl toma la salida git log --name-only HEADy almacena la marca de tiempo más reciente para cualquier archivo mencionado en un registro de confirmación en un hash llamado %files. Ignora los nombres de archivos que no existen.

Luego construye un Hash of Arrays ("HoA" - ver man perldsc) llamado %times, con las marcas de tiempo como clave hash y los valores son una matriz anónima que contiene los nombres de archivos con esa marca de tiempo. Esta es una optimización para que la función táctil solo deba ejecutarse una vez para cada marca de tiempo en lugar de una vez para cada nombre de archivo.

Se ignoran el ID de confirmación, el mensaje de confirmación, el nombre del autor y las líneas en blanco de git logla salida.

El script utiliza la unqqbackslash()función deCadena::Escaparen cada nombre de archivo para manejar correctamente la forma en que git logse imprimen los nombres de archivo con tabulaciones incrustadas, líneas nuevas, comillas dobles, etc. (es decir, como cadenas entre comillas dobles con códigos/caracteres de escape con barra invertida).

Espero que se ejecute docenas de veces más rápido, al menos, que su ciclo bash.

#!/usr/bin/perl

use strict;
use Date::Parse;
use File::Touch;
use String::Escape qw(unqqbackslash);

my %files = ();
my %times = ();
my $t;

while (<>) {
  chomp;
  next if (m/^$|^\s+|^Author: |^commit /);

  if (s/^Date:\s+//) {
    $t = str2time($_);

  } else {
    my $f = unqqbackslash($_);
    next unless -e $f;   # don't create file if it doesn't exist

    if (!defined($files{$f}) || $files{$f} < $t) {
      $files{$f} = $t;
    }

  };
};

# build %files HoA with timestamps containing the
# files modified at that time.
foreach my $f (sort keys %files) {
  push @{ $times{$files{$f}} }, $f;
}

# now touch the files
foreach my $t (keys %times) {
  my $tch = File::Touch->new(mtime_only => 1, time => $t);
  $tch->touch(@{ $times{$t} });
};

El guión utiliza elFecha::Analizar, Archivo::Tocar, yCadena::Escaparmódulos perl.

En Debian, apt install libtimedate-perl libfile-touch-perl libstring-escape-perl. Probablemente otras distribuciones también los tengan empaquetados. De lo contrario, instálelos con cpan.

Ejemplo de uso, en un repositorio de git con un par de archivos basura ( filey file2):

$ git log --date=format:'%Y-%m-%d %H:%M:%S' --pretty='%H  %ad %s' file*
d10c313abb71876cfa8ad420b10f166543ba1402  2021-06-16 14:49:24 updated file2
61799d2c956db37bf56b228da28038841c5cd07d  2021-06-16 13:38:58 added file1
                                                              & file2

$ touch file*
$ ls -l file*
-rw-r--r-- 1 cas cas  5 Jun 16 19:23 file1
-rw-r--r-- 1 cas cas 29 Jun 16 19:23 file2

$ git  log  --name-only HEAD file*  | ./process-git-log.pl 
$ ls -l file*
-rw-r--r-- 1 cas cas  5 Jun 16 13:38 file1
-rw-r--r-- 1 cas cas 29 Jun 16 14:49 file2

(muy ligeramente falsificado: edité los mensajes de confirmación para que quede claro cuándo se confirmaron ambos archivos por primera vez, luego se cambió el archivo 2 y se confirmó nuevamente. Aparte de eso, se copió y pegó directamente desde mi terminal).


Este es mi segundo intento: originalmente intenté usar elGit::Crudomódulo pero no pude encontrar la manera de conseguir que me diera una lista desololos nombres de archivos modificados en una confirmación particular. Estoy seguro de que hay una manera, pero ya me di por vencido. Simplemente no conozco lo gitsuficientemente bien los aspectos internos.

información relacionada