Paralelizar o script Bash com o número máximo de processos

86

Vamos dizer que tenho um loop no Bash:

for foo in `some-command`
do
   do-something $foo
done

do-somethingé vinculado à CPU e eu tenho um processador de 4 núcleos bem brilhante Eu gostaria de poder executar até 4 de do-somethinguma vez.

A abordagem ingênua parece ser:

for foo in `some-command`
do
   do-something $foo &
done

Isso executará todos os do-something s de uma vez, mas existem algumas desvantagens, principalmente que o do-something também pode ter algum I / O significativo que pode ser um pouco mais lento ao executar tudo de uma vez. O outro problema é que esse bloco de código retorna imediatamente, então não há como fazer outro trabalho quando todos os do-somethings estiverem concluídos.

Como você escreveria esse loop para que sempre houvesse do-somethingXs em execução ao mesmo tempo?

thelsdj
fonte
2
Como um sidenode, eu sonhei em adicionar a opção -j do make ao bash para primitivo. Não funcionaria sempre, mas para alguns casos simples em que você sabe que o corpo do loop fará algo único para cada iteração, seria bastante claro apenas dizer "para -j 4 ...".
relaxe
1
Referência cruzada para stackoverflow.com/questions/1537956/… para uma solução bash que mitiga problemas de desempenho e permite grupos de subprocessos, mantidos separados.
paxdiablo
1
Eu recomendaria minha solução stackoverflow.com/a/28965927/340581
Tuttle

Respostas:

62

Dependendo do que você deseja fazer o xargs também pode ajudar (aqui: convertendo documentos com pdf2ps):

cpus=$( ls -d /sys/devices/system/cpu/cpu[[:digit:]]* | wc -w )

find . -name \*.pdf | xargs --max-args=1 --max-procs=$cpus  pdf2ps

Dos documentos:

--max-procs=max-procs
-P max-procs
       Run up to max-procs processes at a time; the default is 1.
       If max-procs is 0, xargs will run as many processes as  possible  at  a
       time.  Use the -n option with -P; otherwise chances are that only one
       exec will be done.
Fritz G. Mehner
fonte
9
Este método, em minha opinião, é a solução mais elegante. Exceto, como sou paranóico, sempre gosto de usar find [...] -print0e xargs -0.
anfetamáquina,
7
cpus=$(getconf _NPROCESSORS_ONLN)
mr.spuratic
1
A partir do manual, por que não usar --max-procs=0para obter o máximo de processos possível?
EverythingRightPlace
@EverythingRightPlace, a pergunta não pede explicitamente mais processos do que os processadores disponíveis. --max-procs=0é mais parecido com a tentativa do questionador (inicie tantos processos quanto argumentos).
Toby Speight
39

Com GNU Parallel http://www.gnu.org/software/parallel/ você pode escrever:

some-command | parallel do-something

GNU Parallel também suporta a execução de trabalhos em computadores remotos. Isso executará um por núcleo de CPU nos computadores remotos - mesmo se eles tiverem um número diferente de núcleos:

some-command | parallel -S server1,server2 do-something

Um exemplo mais avançado: Aqui, listamos os arquivos nos quais queremos que my_script seja executado. Os arquivos têm extensão (talvez .jpeg). Queremos que a saída de my_script seja colocada ao lado dos arquivos em basename.out (por exemplo, foo.jpeg -> foo.out). Queremos executar my_script uma vez para cada núcleo do computador e também queremos executá-lo no computador local. Para os computadores remotos, queremos que o arquivo seja processado e transferido para o computador fornecido. Quando my_script terminar, queremos que foo.out seja transferido de volta e, em seguida, que foo.jpeg e foo.out sejam removidos do computador remoto:

cat list_of_files | \
parallel --trc {.}.out -S server1,server2,: \
"my_script {} > {.}.out"

O GNU Parallel garante que a saída de cada trabalho não seja combinada, então você pode usar a saída como entrada para outro programa:

some-command | parallel do-something | postprocess

Veja os vídeos para mais exemplos: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Ole Tange
fonte
1
Observe que isso é realmente útil ao usar um findcomando para gerar uma lista de arquivos, porque não apenas evita o problema quando há um espaço dentro de um nome de arquivo que ocorre em, for i in ...; domas o find também pode fazer o find -name \*.extension1 -or -name \*.extension2que o GNU parallel {.} Pode lidar muito bem.
Leo Izen
Mais 1, embora catseja, obviamente, inútil.
tripleee
@tripleee Re: Uso inútil de cat. Consulte oletange.blogspot.dk/2013/10/useless-use-of-cat.html
Ole Tange
Ah é você! A propósito, você poderia atualizar o link desse blog? O local do partmaps.org lamentavelmente está morto, mas o redirecionador Iki deve continuar a funcionar.
tripleee
22
maxjobs = 4
paralelizar () {
        enquanto [$ # -gt 0]; Faz
                jobcnt = (`jobs -p`)
                if [$ {# jobcnt [@]} -lt $ maxjobs]; então
                        faça algo $ 1 &
                        mudança  
                outro
                        dormir 1
                fi
        feito
        esperar
}

paralelizar arg1 arg2 "5 args para a terceira tarefa" arg4 ...
bstark
fonte
10
Perceba que há sérios subquotes acontecendo aqui, então qualquer trabalho que requeira espaços nos argumentos falhará muito; além disso, este script consumirá sua CPU enquanto aguarda a conclusão de alguns trabalhos, se forem solicitados mais trabalhos do que o permitido por maxjobs.
lhunath
1
Observe também que isso pressupõe que seu script não está fazendo mais nada a ver com os trabalhos; se você estiver, isso contará para maxjobs também.
lhunath
1
Você pode querer usar "jobs -pr" para limitar a execução de jobs.
anfetamáquina,
1
Adicionado um comando sleep para evitar que o loop while se repita sem nenhuma interrupção, enquanto ele espera que os comandos do-something já em execução sejam concluídos. Caso contrário, este loop ocuparia essencialmente um dos núcleos da CPU. Isso também aborda a preocupação de @lhunath.
euforia83
12

Aqui está uma solução alternativa que pode ser inserida em .bashrc e usada para um liner diário:

function pwait() {
    while [ $(jobs -p | wc -l) -ge $1 ]; do
        sleep 1
    done
}

Para usá-lo, basta colocar &após os jobs e uma chamada pwait, o parâmetro fornece o número de processos paralelos:

for i in *; do
    do_something $i &
    pwait 10
done

Seria melhor usar em waitvez de ficar ocupado aguardando a saída de jobs -p, mas não parece haver uma solução óbvia para esperar até que qualquer um dos trabalhos fornecidos seja concluído em vez de todos eles.

Grumbel
fonte
11

Em vez de um bash simples, use um Makefile e especifique o número de trabalhos simultâneos com, em make -jXque X é o número de trabalhos a serem executados de uma vez.

Ou você pode usar wait(" man wait"): iniciar vários processos filhos, chamar wait- ele sairá quando os processos filhos terminarem.

maxjobs = 10

foreach line in `cat file.txt` {
 jobsrunning = 0
 while jobsrunning < maxjobs {
  do job &
  jobsrunning += 1
 }
wait
}

job ( ){
...
}

Se você precisar armazenar o resultado do trabalho, atribua o resultado a uma variável. Depois waité só verificar o que a variável contém.

Skolima
fonte
1
Obrigado por isso, embora o código não esteja concluído, ele me deu a resposta para um problema que estou tendo no trabalho.
gerikson
o único problema é que se você matar o script de primeiro plano (aquele com o loop), os jobs que estavam em execução não serão eliminados juntos
Girardi
8

Talvez tente um utilitário de paralelização em vez de reescrever o loop? Sou um grande fã de xjobs. Eu uso xjobs o tempo todo para copiar arquivos em massa em nossa rede, geralmente ao configurar um novo servidor de banco de dados. http://www.maier-komor.de/xjobs.html

Tessein
fonte
7

Se você está familiarizado com o makecomando, na maioria das vezes pode expressar a lista de comandos que deseja executar como um makefile. Por exemplo, se você precisa executar $ SOME_COMMAND em arquivos * .input, cada um dos quais produz * .output, você pode usar o makefile

INPUT = a.input b.input
OUTPUT = $ (INPUT: .input = .output)

%.saída entrada
    $ (SOME_COMMAND) $ <$ @

todos: $ (SAÍDA)

e então apenas corra

make -j <NUMBER>

para executar no máximo NUMBER comandos em paralelo.

Idelic
fonte
6

Embora bashseja provavelmente impossível fazer isso direito , você pode fazer um semi-certo com bastante facilidade. bstarkdeu uma boa aproximação do direito, mas tem as seguintes falhas:

  • Divisão de palavras: você não pode passar nenhuma tarefa que use qualquer um dos seguintes caracteres em seus argumentos: espaços, tabulações, novas linhas, estrelas, pontos de interrogação. Se você fizer isso, as coisas vão quebrar, possivelmente de forma inesperada.
  • Ele depende do resto do seu script para não colocar nada em segundo plano. Se você fizer isso, ou mais tarde, adicionar algo ao script que é enviado em segundo plano porque você esqueceu que não tinha permissão para usar trabalhos em segundo plano por causa de seu snippet, as coisas vão quebrar.

Outra aproximação que não tem essas falhas é a seguinte:

scheduleAll() {
    local job i=0 max=4 pids=()

    for job; do
        (( ++i % max == 0 )) && {
            wait "${pids[@]}"
            pids=()
        }

        bash -c "$job" & pids+=("$!")
    done

    wait "${pids[@]}"
}

Observe que este é facilmente adaptável para também verificar o código de saída de cada trabalho assim que termina, para que você possa avisar o usuário se um trabalho falhar ou definir um código de saída de scheduleAllacordo com a quantidade de trabalhos que falharam, ou algo assim.

O problema com este código é apenas este:

  • Ele agenda quatro (neste caso) jobs por vez e, em seguida, espera que todos os quatro terminem. Alguns podem ser feitos mais cedo do que outros, o que fará com que o próximo lote de quatro trabalhos espere até que o mais longo do lote anterior seja concluído.

Uma solução que cuida desse último problema teria que usar kill -0para pesquisar se algum dos processos desapareceu em vez de waite agendar o próximo trabalho. No entanto, isso introduz um pequeno problema novo: você tem uma condição de corrida entre o término de um trabalho e a kill -0verificação se ele foi encerrado. Se o trabalho terminar e outro processo em seu sistema iniciar ao mesmo tempo, pegando um PID aleatório que por acaso é o do trabalho que acabou de terminar, o kill -0usuário não notará que seu trabalho foi concluído e as coisas vão quebrar novamente.

Uma solução perfeita não é possível em bash.

lhunath
fonte
3

função para bash:

parallel ()
{
    awk "BEGIN{print \"all: ALL_TARGETS\\n\"}{print \"TARGET_\"NR\":\\n\\t@-\"\$0\"\\n\"}END{printf \"ALL_TARGETS:\";for(i=1;i<=NR;i++){printf \" TARGET_%d\",i};print\"\\n\"}" | make $@ -f - all
}

usando:

cat my_commands | parallel -j 4
ilnar
fonte
O uso de make -jé inteligente, mas sem nenhuma explicação e aquele blob de código Awk somente para gravação, evito votos positivos.
tripleee
2

O projeto no qual trabalho usa o comando wait para controlar processos shell paralelos (ksh na verdade). Para atender às suas preocupações sobre IO, em um sistema operacional moderno, é possível que a execução paralela aumente a eficiência. Se todos os processos estiverem lendo os mesmos blocos no disco, apenas o primeiro processo terá que atingir o hardware físico. Os outros processos freqüentemente serão capazes de recuperar o bloco do cache de disco do sistema operacional na memória. Obviamente, ler da memória é várias ordens de magnitude mais rápido do que ler do disco. Além disso, o benefício não requer alterações de codificação.

Jon Ericson
fonte
1

Isso pode ser bom o suficiente para a maioria dos propósitos, mas não é o ideal.

#!/bin/bash

n=0
maxjobs=10

for i in *.m4a ; do
    # ( DO SOMETHING ) &

    # limit jobs
    if (( $(($((++n)) % $maxjobs)) == 0 )) ; then
        wait # wait until all have finished (not optimal, but most times good enough)
        echo $n wait
    fi
done
gato
fonte
1

Aqui está como consegui resolver esse problema em um script bash:

 #! /bin/bash

 MAX_JOBS=32

 FILE_LIST=($(cat ${1}))

 echo Length ${#FILE_LIST[@]}

 for ((INDEX=0; INDEX < ${#FILE_LIST[@]}; INDEX=$((${INDEX}+${MAX_JOBS})) ));
 do
     JOBS_RUNNING=0
     while ((JOBS_RUNNING < MAX_JOBS))
     do
         I=$((${INDEX}+${JOBS_RUNNING}))
         FILE=${FILE_LIST[${I}]}
         if [ "$FILE" != "" ];then
             echo $JOBS_RUNNING $FILE
             ./M22Checker ${FILE} &
         else
             echo $JOBS_RUNNING NULL &
         fi
         JOBS_RUNNING=$((JOBS_RUNNING+1))
     done
     wait
 done
Fernando
fonte
1

Muito tarde para a festa aqui, mas aqui está outra solução.

Muitas soluções não lidam com espaços / caracteres especiais nos comandos, não mantêm N jobs em execução o tempo todo, comem cpu em loops ocupados ou dependem de dependências externas (por exemplo, GNU parallel).

Com inspiração para manipulação de processos mortos / zumbis , aqui está uma solução puramente bash:

function run_parallel_jobs {
    local concurrent_max=$1
    local callback=$2
    local cmds=("${@:3}")
    local jobs=( )

    while [[ "${#cmds[@]}" -gt 0 ]] || [[ "${#jobs[@]}" -gt 0 ]]; do
        while [[ "${#jobs[@]}" -lt $concurrent_max ]] && [[ "${#cmds[@]}" -gt 0 ]]; do
            local cmd="${cmds[0]}"
            cmds=("${cmds[@]:1}")

            bash -c "$cmd" &
            jobs+=($!)
        done

        local job="${jobs[0]}"
        jobs=("${jobs[@]:1}")

        local state="$(ps -p $job -o state= 2>/dev/null)"

        if [[ "$state" == "D" ]] || [[ "$state" == "Z" ]]; then
            $callback $job
        else
            wait $job
            $callback $job $?
        fi
    done
}

E uso de amostra:

function job_done {
    if [[ $# -lt 2 ]]; then
        echo "PID $1 died unexpectedly"
    else
        echo "PID $1 exited $2"
    fi
}

cmds=( \
    "echo 1; sleep 1; exit 1" \
    "echo 2; sleep 2; exit 2" \
    "echo 3; sleep 3; exit 3" \
    "echo 4; sleep 4; exit 4" \
    "echo 5; sleep 5; exit 5" \
)

# cpus="$(getconf _NPROCESSORS_ONLN)"
cpus=3
run_parallel_jobs $cpus "job_done" "${cmds[@]}"

A saída:

1
2
3
PID 56712 exited 1
4
PID 56713 exited 2
5
PID 56714 exited 3
PID 56720 exited 4
PID 56724 exited 5

Para manipulação de saída por processo $$pode ser usado para registrar em um arquivo, por exemplo:

function job_done {
    cat "$1.log"
}

cmds=( \
    "echo 1 \$\$ >\$\$.log" \
    "echo 2 \$\$ >\$\$.log" \
)

run_parallel_jobs 2 "job_done" "${cmds[@]}"

Resultado:

1 56871
2 56872
Skrat
fonte
0

Você pode usar um simples loop for aninhado (substitua N e M por inteiros apropriados abaixo):

for i in {1..N}; do
  (for j in {1..M}; do do_something; done & );
done

Isso executará do_something N * M vezes em M rodadas, cada rodada executando N tarefas em paralelo. Você pode tornar N igual ao número de CPUs que possui.

Adam Zalcman
fonte
0

Minha solução para sempre manter um determinado número de processos em execução, manter o rastreamento de erros e lidar com processos ubinterruptíveis / zumbis:

function log {
    echo "$1"
}

# Take a list of commands to run, runs them sequentially with numberOfProcesses commands simultaneously runs
# Returns the number of non zero exit codes from commands
function ParallelExec {
    local numberOfProcesses="${1}" # Number of simultaneous commands to run
    local commandsArg="${2}" # Semi-colon separated list of commands

    local pid
    local runningPids=0
    local counter=0
    local commandsArray
    local pidsArray
    local newPidsArray
    local retval
    local retvalAll=0
    local pidState
    local commandsArrayPid

    IFS=';' read -r -a commandsArray <<< "$commandsArg"

    log "Runnning ${#commandsArray[@]} commands in $numberOfProcesses simultaneous processes."

    while [ $counter -lt "${#commandsArray[@]}" ] || [ ${#pidsArray[@]} -gt 0 ]; do

        while [ $counter -lt "${#commandsArray[@]}" ] && [ ${#pidsArray[@]} -lt $numberOfProcesses ]; do
            log "Running command [${commandsArray[$counter]}]."
            eval "${commandsArray[$counter]}" &
            pid=$!
            pidsArray+=($pid)
            commandsArrayPid[$pid]="${commandsArray[$counter]}"
            counter=$((counter+1))
        done


        newPidsArray=()
        for pid in "${pidsArray[@]}"; do
            # Handle uninterruptible sleep state or zombies by ommiting them from running process array (How to kill that is already dead ? :)
            if kill -0 $pid > /dev/null 2>&1; then
                pidState=$(ps -p$pid -o state= 2 > /dev/null)
                if [ "$pidState" != "D" ] && [ "$pidState" != "Z" ]; then
                    newPidsArray+=($pid)
                fi
            else
                # pid is dead, get it's exit code from wait command
                wait $pid
                retval=$?
                if [ $retval -ne 0 ]; then
                    log "Command [${commandsArrayPid[$pid]}] failed with exit code [$retval]."
                    retvalAll=$((retvalAll+1))
                fi
            fi
        done
        pidsArray=("${newPidsArray[@]}")

        # Add a trivial sleep time so bash won't eat all CPU
        sleep .05
    done

    return $retvalAll
}

Uso:

cmds="du -csh /var;du -csh /tmp;sleep 3;du -csh /root;sleep 10; du -csh /home"

# Execute 2 processes at a time
ParallelExec 2 "$cmds"

# Execute 4 processes at a time
ParallelExec 4 "$cmds"
Orsiris de Jong
fonte
-1

$ DOMAINS = "lista de alguns domínios em comandos" para foo in some-command do

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

feito

Ndomains =echo $DOMAINS |wc -w

para i em $ (seq 1 1 $ Ndomains) faça echo "esperar por $ {job [$ i]}" esperar "$ {job [$ i]}" concluído

neste conceito funcionará para o paralelizar. o importante é que a última linha de avaliação é '&', que colocará os comandos em segundo plano.

Jack
fonte