Processos paralelos: anexando saídas a um array em um script bash

4

Eu tenho um loop for no qual uma função task é chamada. Cada chamada para a função retorna uma string anexada a uma matriz. Eu gostaria de paralelizar este loop. Eu tentei usar & , mas parece que não funciona.

Aqui está o código não paralelizado.

task (){ sleep 1;echo "hello $1"; }
arr=()

for i in {1..3}; do
    arr+=("$(task $i)")
done

for i in "${arr[@]}"; do
    echo "$i x";
done

A saída é:

hello 1 x
hello 2 x
hello 3 x

Ótimo! Mas agora, quando tento paralelizá-lo com

[...]
for i in {1..3}; do
    arr+=("$(task $i)")&
done
wait
[...]

a saída está vazia.

UPDATE # 1

Em relação à função task :

  • A função task leva algum tempo para ser executada e, em seguida, exibe uma sequência. Depois que todas as strings forem reunidas, outro loop for percorrerá as strings e executará alguma outra tarefa.
  • O pedido não importa. A cadeia de saída pode consistir em uma cadeia de linha única, possivelmente com várias palavras separadas por um espaço em branco.
por BiBi 26.07.2018 / 18:57

3 respostas

3

Você não pode enviar uma atribuição para o segundo plano, já que o processo em segundo plano é uma bifurcação do shell, e as alterações na variável não são visíveis no shell principal.

Mas você pode executar várias tarefas em paralelo, fazer com que todas saiam para um canal e depois ler o que aparecer. Ou, na verdade, use substituição de processos , para evitar a emissão de comandos em um pipe sendo executados em uma subcamada (consulte < a href="https://unix.stackexchange.com/q/9954/170373"> Por que minha variável local está em um loop 'while read', mas não em outro loop aparentemente similar? )

Contanto que as saídas sejam linhas individuais escritas atomicamente, elas não serão mescladas, mas podem ser reordenadas:

$ task() { sleep 1; echo "$1"; }
$ time while read -r line; do arr+=("$line"); done < <(for x in 1 2 3 ; do task "$x" & done)
real    0m1.006s
$ declare -p arr
declare -a arr=([0]="2" [1]="1" [2]="3")

O acima irá executar todas as tarefas ao mesmo tempo. Há também o paralelo GNU (e -P no GNU xargs), que é exatamente para executar tarefas em paralelo, e só vai correr alguns ao mesmo tempo. O Parallel também armazena em buffer as saídas das tarefas, para que você não obtenha dados misturados, mesmo que a tarefa grave linhas em partes.

$ mapfile -t arr < <(parallel -j4 bash ./task.sh ::: {a,b,c})
$ declare -p arr
declare -a arr=([0]="a" [1]="b" [2]="c")

(O mapfile do Bash aqui lê as linhas de entrada para o array, similarmente ao loop while read .. arr+=() acima).

A execução de um script externo como acima é simples, mas você pode realmente executar uma função exportada também, embora, é claro, todas as tarefas sejam executadas em cópias independentes do shell, para que tenham suas próprias cópias de cada variável, etc.

$ export -f task
$ mapfile -t arr < <(parallel task ::: {a,b,c})

O exemplo acima aconteceu para manter a , b e c em ordem, mas isso é uma coincidência. Use parallel -k para garantir que as saídas sejam mantidas em ordem.

    
por 26.07.2018 / 21:33
0

Uma maneira um pouco ingênua, mas robusta, de fazer isso que é portátil em shells semelhantes a Bourne:

#!/bin/sh

task () {
    tid="$1"
    printf 'tid %d: Running...\n' "$tid"
    sleep "$(( RANDOM % 5 ))"
    printf 'tid %d: Done.\n' "$tid"
}

ntasks=10

tid=0
while [ "$tid" -ne "$ntasks" ]; do
    tid=$(( tid + 1 ))
    printf 'main: Starting task with tid=%d\n' "$tid"
    task "$tid" >"output.$tid" 2>&1 &
done

wait

tid=0
while [ "$tid" -ne "$ntasks" ]; do
    tid=$(( tid + 1 ))
    printf 'main: Processing output from task with tid=%d\n' "$tid"
    # do something with "output.$tid"
done

Isso gera as tarefas no primeiro loop e aguarda que sejam concluídas antes de processar a saída no segundo loop. Isso seria adequado se as tarefas gerassem muitos dados.

Para limitar o número de tarefas em execução a no máximo 4, o loop inicial pode ser alterado para

tid=0
while [ "$tid" -ne "$ntasks" ]; do
    tid=$(( tid + 1 ))
    printf 'main: Starting task with tid=%d\n' "$tid"
    task "$tid" >"output.$tid" 2>&1 &

    if [ "$(( tid % 4 ))" -eq 0 ]; then
        wait
    fi
done
    
por 27.07.2018 / 12:16
0

Você está procurando por parset (parte do GNU Parallel desde 20170422) ou env_parset (disponível desde 20171222):

# If you have not run:
#    env_parallel --install
# and logged in again, then you can instead run this to activate (env_)parset:
. 'which env_parallel.bash'

task (){
  echo "hello $1"
  sleep 1.$1
  perl -e 'print "binary
# If you have not run:
#    env_parallel --install
# and logged in again, then you can instead run this to activate (env_)parset:
. 'which env_parallel.bash'

task (){
  echo "hello $1"
  sleep 1.$1
  perl -e 'print "binary%pre%1%pre%2\n"'
  sleep 1.$1
  echo output of parallel jobs do not mix
}
env_parset arr task ::: {1..3}
env_parset a,b,c task ::: {1..3}

echo "${arr[1]}" | xxd
echo "$b" | xxd
1%pre%2\n"' sleep 1.$1 echo output of parallel jobs do not mix } env_parset arr task ::: {1..3} env_parset a,b,c task ::: {1..3} echo "${arr[1]}" | xxd echo "$b" | xxd

parset é suportado em Bash / Ksh / Zsh (incluindo matrizes), cinza / traço (sem matrizes).

    
por 28.07.2018 / 18:25