Paralelizar script Bash com número máximo de processos
-
09-06-2019 - |
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-something
s 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-something
estão concluídos.
Como você escreveria esse loop para que sempre houvesse X do-something
está funcionando de uma vez?
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-command
fazer
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.