Pergunta

Digamos que eu tenha um loop no Bash:

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

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

A abordagem ingênua parece ser:

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

Isso será executado todos do-somethings de uma vez, mas há algumas desvantagens, principalmente que do-something também pode ter alguma E/S significativa que executa todos de uma vez pode desacelerar um pouco.O outro problema é que esse bloco de código retorna imediatamente, então não há como fazer outro trabalho quando todos os do-somethingestão concluídos.

Como você escreveria esse loop para que sempre houvesse X do-somethingestá funcionando de uma vez?

Foi útil?

Solução

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.

Outras dicas

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

some-command | parallel do-something

GNU Parallel também oferece suporte à execução de trabalhos em computadores remotos.Isso executará um por núcleo de CPU nos computadores remotos - mesmo que eles tenham 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 exemplofoo.jpeg -> foo.out).Queremos executar my_script uma vez para cada núcleo que o computador possui e também queremos executá-lo no computador local.Para os computadores remotos, queremos que o arquivo seja processado e transferido para o computador específico.Quando my_script terminar, queremos que foo.out seja transferido de volta e então foo.jpeg e foo.out removidos do computador remoto:

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

GNU Parallel garante que a saída de cada trabalho não se misture, 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

maxjobs=4
parallelize () {
        while [ $# -gt 0 ] ; do
                jobcnt=(`jobs -p`)
                if [ ${#jobcnt[@]} -lt $maxjobs ] ; then
                        do-something $1 &
                        shift  
                else
                        sleep 1
                fi
        done
        wait
}

parallelize arg1 arg2 "5 args to third job" arg4 ...

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

Ou você pode usar wait ("man wait"):lançar 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 você apenas verifica o que a variável contém.

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

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

Para utilizá-lo, basta colocar & após os trabalhos 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 wait em vez de ficar ocupado esperando 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.

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

Ao fazer isso direito em bash é provavelmente impossível, você pode fazer um semi-certo com bastante facilidade. bstark deu uma aproximação justa do direito, mas ele tem as seguintes falhas:

  • Divisão de palavras:Você não pode passar para ele nenhum trabalho 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 irão quebrar, possivelmente de forma inesperada.
  • 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 irão quebrar.

Outra aproximação que não apresenta 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 verificar também o código de saída de cada trabalho quando ele termina, para que você possa avisar o usuário se um trabalho falhar ou definir um código de saída para scheduleAll de acordo com a quantidade de trabalhos que falharam, ou algo assim.

O problema com este código é apenas isso:

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

Uma solução que resolva esta última questão teria que usar kill -0 para pesquisar se algum dos processos desapareceu em vez do wait e agende 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 o kill -0 verificando se terminou.Se o trabalho terminar e outro processo no seu sistema for iniciado ao mesmo tempo, pegando um PID aleatório que é o do trabalho que acabou de terminar, o kill -0 não notará que seu trabalho terminou e as coisas vão quebrar novamente.

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

Se você estiver familiarizado com o make comando, na maioria das vezes você pode expressar a lista de comandos que deseja executar como um makefile.Por exemplo, se você precisar 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)

%.output : %.input
    $(SOME_COMMAND) $< $@

all: $(OUTPUT)

e então é só correr

make -j<NUMBER>

para executar no máximo NUMBER comandos em paralelo.

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

O projeto em que trabalho usa o espere comando para controlar processos de shell paralelo (ksh, na verdade).Para resolver suas preocupações sobre IO, em um sistema operacional moderno, é possível que a execução paralela realmente 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 geralmente serão capazes de recuperar o bloco do cache de disco do sistema operacional na memória.Obviamente, a leitura da memória é várias ordens de grandeza mais rápida do que a leitura do disco.Além disso, o benefício não requer alterações de codificação.

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

Realmente atrasado 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, consomem 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 bash pura:

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[@]}"

Saída:

1 56871
2 56872

Você pode usar um loop for aninhado simples (substitua os números inteiros apropriados por N e M 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 jobs em paralelo.Você pode fazer com que N seja igual ao número de CPUs que você possui.

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

Minha solução para manter sempre um determinado número de processos em execução, rastrear erros e lidar com processos ininterruptos/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"

$ Domains = "Lista de algum domínio em comandos" para foo em some-commandfazer

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

feito

Ndomínios=echo $DOMAINS |wc -w

para i em $ (seq 1 1 $ nDomains) faça eco "aguarde $ {job [$ i]}" wait "$ {job [$ i]}" feito

neste conceito funcionará para o paralelismo.A coisa importante é a última linha de avaliação é '&', que colocará os comandos nos antecedentes.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top