Универсальное преобразование/замена `во время чтения чего-либо` в `параллельное`

Универсальное преобразование/замена `во время чтения чего-либо` в `параллельное`

У меня есть куча скриптов, которые делают это:

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

Кто-нибудь когда-нибудь задумывался о том, как запустить все команды, проходящие через конвейер, используя parallel?для этого случаярешения этой проблемы, но я ищу что-тообщийкоторый требует лишь минимальных изменений в моих скриптах и, если возможно, позволяет работать даже если parallelон не установлен.

commandвыше может быть практически все, не ограничиваясь одной командой. a long list of commandsможет быть и совершенно другим.

Например, рассмотрим этот однострочный код, который изменяет даты изменения извлеченных файлов в репозитории git на дату их последнего изменения:

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

По умолчанию это ужасно медленно, потому что git logи touchобе команды довольно медленные. Но это всего лишь один пример, и притом простой.

решение1

Я бы использовал функцию bash и вызвал бы ее:

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

Используйте parallel -0, если хотите разделить по NUL.

Если вы хотите запустить вышеизложенное без установки GNU Parallel, вы можете использовать:

parallel --embed > myscript.sh

а затем добавьте вышеизложенное к myscript.sh.

решение2

Можно заставить bash или ksh одновременно запускать список независимых команд, так что каждый поток начинает новую команду, как только его предыдущая задача завершается. Потоки остаются занятыми, за исключением команд хвостовой части.

Основной метод заключается в запуске нескольких асинхронных оболочек, которые все считывают данные из одного и того же канала: канал гарантирует буферизацию строк и атомарное чтение (файл команд может использоваться с cat file |перенаправлением, но не посредством него).

Команды могут быть любыми однострочными командами оболочки (в правильном синтаксисе для оболочки, владеющей потоком), но не могут полагаться на результаты предыдущих команд, поскольку распределение команд по потокам является произвольным. Сложные команды лучше всего настраивать как внешние скрипты, чтобы их можно было вызывать как простую команду с аргументами.

Это тестовый запуск шести заданий в трех потоках, иллюстрирующий перекрытие заданий. (Я также провел стресс-тест 240 заданий в 80 потоках на своем ноутбуке.)

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

Это прокси-скрипт, который обеспечивает отладку этих заданий.

#! /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}"

Это скрипт управления потоком. Он создает команды задания для запуска прокси и инициирует фоновые оболочки.

#! /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

решение3

Вместо того, чтобы запускать git ls-treeи затем git log, date, и touchнесколько раз в цикле bash while read, следующий скрипт perl берет вывод git log --name-only HEADи сохраняет самую последнюю временную метку для любого файла, упомянутого в журнале коммитов, в хэше с именем %files. Он игнорирует имена файлов, которые не существуют.

Затем он создает хэш массивов («HoA» — см. man perldsc), называемый %times, с временными метками в качестве ключа хэша и значениями, представляющими собой анонимный массив, содержащий имена файлов с этой временной меткой. Это оптимизация, так что сенсорную функцию нужно запускать только один раз для каждой временной метки, а не один раз для каждого имени файла.

Идентификатор коммита, сообщение о коммите, имя автора и пустые строки из git logвыходных данных игнорируются.

Скрипт использует unqqbackslash()функцию изСтрока::Escapeдля каждого имени файла для правильной обработки способа git logпечати имен файлов со встроенными символами табуляции, новой строки, двойными кавычками и т. д. (т. е. как строк в двойных кавычках с экранированными кодами/символами обратной косой черты).

Я ожидаю, что он будет работать как минимум в десятки раз быстрее, чем ваш 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} });
};

Скрипт используетДата::Анализ, Файл::Touch, иСтрока::Escapeмодули perl.

В Debian, apt install libtimedate-perl libfile-touch-perl libstring-escape-perl. Другие дистрибутивы, вероятно, также имеют их в пакетах. В противном случае установите их с помощью cpan.

Пример использования в репозитории git с парой ненужных файлов ( file, и 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

(совсем немного подделано — я отредактировал сообщения о коммите, чтобы было понятно, когда оба файла были впервые зафиксированы, а затем file2 был изменён и снова зафиксирован. В остальном это просто копипаст из моего терминала).


Это моя вторая попытка: изначально я пытался использоватьGit::Сыроймодуль, но не могу понять, как заставить его выдать мне списоктолькоимена файлов, измененные в определенном коммите. Я уверен, что есть способ, но я отказался от этого. Я просто недостаточно хорошо знаю внутренности git.

Связанный контент