Zrównoleglenie skryptu Bash z maksymalną liczbą procesów


86

Powiedzmy, że mam pętlę w Bash:

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

do-somethingjest związany z procesorem i mam ładny błyszczący 4-rdzeniowy procesor. Chciałbym móc biegać do czterech do-somethingna raz.

Wydaje się, że naiwne podejście brzmi:

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

Spowoduje to uruchomienie wszystkich do-something s naraz, ale istnieje kilka wad, głównie że zrób coś może mieć również pewne znaczące I / O, które wykonując wszystkie naraz może spowolnić trochę. Innym problemem jest to, że ten blok kodu wraca natychmiast, więc nie ma możliwości wykonania innej pracy po zakończeniu wszystkich do-somethings.

Jak napisałeś tę pętlę, aby zawsze były do-somethinguruchomione X- y na raz?


2
Jako sidenode marzyłem o dodaniu opcji make -j do basha dla prymitywu. Nie zawsze to zadziałałoby, ale w niektórych prostych przypadkach, w których wiadomo, że główna część pętli zrobi coś unikalnego dla każdej iteracji, byłoby całkiem nieźle powiedzieć „for -j 4…”.
odpocząć

1
Odsyłacz do stackoverflow.com/questions/1537956/… dla rozwiązania bash, które łagodzi problemy z wydajnością i pozwala na oddzielne grupy podprocesów.
paxdiablo

1
Poleciłbym moje rozwiązanie stackoverflow.com/a/28965927/340581
Tuttle

Odpowiedzi:


62

W zależności od tego, co chcesz zrobić, xargs również może pomóc (tutaj: konwersja dokumentów z pdf2ps):

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

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

Z dokumentów:

--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.

9
Ta metoda jest moim zdaniem najbardziej eleganckim rozwiązaniem. Z wyjątkiem tego, że jestem paranoikiem, zawsze lubię używać find [...] -print0i xargs -0.
amfetamachine

7
cpus=$(getconf _NPROCESSORS_ONLN)
pan spuratic

1
Dlaczego nie skorzystać z instrukcji, --max-procs=0aby uzyskać jak najwięcej procesów?
EverythingRightPlace

@EverythingRightPlace, pytanie wyraźnie dotyczy nie więcej procesów niż dostępnych procesorów. --max-procs=0bardziej przypomina próbę pytającego (uruchom tyle procesów, ile argumentów).
Toby Speight

39

Dzięki GNU Parallel http://www.gnu.org/software/parallel/ możesz pisać:

some-command | parallel do-something

GNU Parallel obsługuje również uruchamianie zadań na komputerach zdalnych. Spowoduje to uruchomienie jednego na rdzeń procesora na zdalnych komputerach - nawet jeśli mają one inną liczbę rdzeni:

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

Bardziej zaawansowany przykład: Tutaj podajemy listę plików, na których ma działać my_script. Pliki mają rozszerzenie (może .jpeg). Chcemy, aby dane wyjściowe my_script były umieszczone obok plików w basename.out (np. Foo.jpeg -> foo.out). Chcemy uruchomić my_script raz dla każdego rdzenia komputera i chcemy go również uruchomić na komputerze lokalnym. W przypadku komputerów zdalnych chcemy, aby plik został przetworzony i przesłany na dany komputer. Po zakończeniu działania my_script chcemy przenieść foo.out z powrotem, a następnie usunąć foo.jpeg i foo.out ze zdalnego komputera:

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

GNU Parallel zapewnia, że ​​dane wyjściowe z każdego zadania nie są mieszane, więc możesz użyć wyjścia jako wejścia dla innego programu:

some-command | parallel do-something | postprocess

Zobacz filmy, aby zobaczyć więcej przykładów: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1


1
Zauważ, że jest to naprawdę przydatne, gdy używasz findpolecenia do generowania listy plików, ponieważ nie tylko zapobiega problemowi, gdy występuje spacja wewnątrz nazwy pliku, który występuje w pliku, for i in ...; doale find może również zrobić to, find -name \*.extension1 -or -name \*.extension2co {.} Radzi sobie bardzo dobrze w GNU równolegle.
Leo Izen

Plus 1 choć catjest oczywiście bezużyteczny.
tripleee

@tripleee Re: Bezużyteczne użycie cat. Zobacz oletange.blogspot.dk/2013/10/useless-use-of-cat.html
Ole Tange

Och, to ty! Nawiasem mówiąc, czy mógłbyś zaktualizować link na tym blogu? Lokalizacja partmaps.org niestety nie żyje, ale readresator Iki powinien nadal działać.
tripleee

22
maxjobs = 4
zrównoleglenie () {
        podczas [$ # -gt 0]; zrobić
                jobcnt = (`Jobs -p`)
                if [$ {# jobcnt [@]} -lt $ maxjobs]; następnie
                        zrób coś 1 $ &
                        Zmiana  
                jeszcze
                        spać 1
                fi
        Gotowe
        czekać
}

równolegle arg1 arg2 "5 argumentów do trzeciego zadania" arg4 ...

10
Uświadom sobie, że dzieje się tutaj kilka poważnych podtekstów, więc wszelkie prace wymagające spacji w argumentach zakończą się niepowodzeniem; co więcej, ten skrypt pożre twój procesor żywcem, czekając na zakończenie niektórych zadań, jeśli żądanych jest więcej zadań niż pozwala na to maxjobs.
lhunath

1
Zauważ również, że zakłada to, że twój skrypt nie robi nic innego, co ma do czynienia z zadaniami; jeśli tak, to również zalicza je do maxjobów.
lhunath

1
Możesz użyć "jobs -pr", aby ograniczyć się do uruchomionych zadań.
amfetamachine

1
Dodano polecenie usypiania, aby zapobiec powtarzaniu się pętli while bez przerwy, gdy czeka na zakończenie już uruchomionych poleceń zrób coś. W przeciwnym razie ta pętla zasadniczo zajęłaby jeden z rdzeni procesora. To również rozwiązuje problem @lhunath.
euphoria83,

12

Tutaj alternatywne rozwiązanie, które można włożyć do .bashrc i zastosować na co dzień jeden wkład:

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

Aby go użyć, wystarczy umieścić &po zadaniach i wywołaniu pwait, parametr podaje liczbę równoległych procesów:

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

Byłoby przyjemniejsze w użyciu waitzamiast czekania na wyjście jobs -p, ale nie wydaje się oczywistym rozwiązaniem czekania, aż którekolwiek z podanych zadań zostanie zakończone zamiast wszystkich.


11

Zamiast zwykłego basha użyj pliku Makefile, a następnie określ liczbę jednoczesnych zadań, make -jXgdzie X jest liczbą zadań do jednoczesnego uruchomienia.

Możesz też użyć wait(" man wait"): uruchom kilka procesów potomnych, wywołaj wait- zakończy się po zakończeniu procesów potomnych.

maxjobs = 10

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

job ( ){
...
}

Jeśli chcesz zapisać wynik zadania, przypisz ich wynik do zmiennej. Po waitsprawdzeniu, co zawiera zmienna.


1
Dzięki za to, mimo że kod nie jest skończony, dał mi odpowiedź na problem, który mam w pracy.
gerikson

jedyny problem polega na tym, że jeśli zabijesz skrypt pierwszego planu (ten z pętlą), uruchomione zadania nie zostaną zabite razem
Girardi

8

Może spróbuj narzędzia równoległego zamiast przepisywania pętli? Jestem wielkim fanem xjobs. Cały czas używam xjobs do masowego kopiowania plików w naszej sieci, zwykle podczas konfigurowania nowego serwera bazy danych. http://www.maier-komor.de/xjobs.html


7

Jeśli znasz makepolecenie, przez większość czasu możesz wyrazić listę poleceń, które chcesz uruchomić, jako plik makefile. Na przykład, jeśli chcesz uruchomić $ SOME_COMMAND na plikach * .input, z których każdy daje * .output, możesz użyć makefile

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

%.wyjście wejście
    $ (SOME_COMMAND) $ <$ @

all: $ (WYJŚCIE)

a potem po prostu biegnij

make -j <LICZBA>

aby uruchomić co najwyżej NUMBER poleceń równolegle.


6

Chociaż zrobienie tego od razu bashjest prawdopodobnie niemożliwe, możesz dość łatwo zrobić półprawo. bstarkdał dobre przybliżenie racji, ale ma następujące wady:

  • Dzielenie słów: nie możesz przekazać mu żadnych zadań, które używają któregokolwiek z następujących znaków w swoich argumentach: spacje, tabulatory, znaki nowej linii, gwiazdki, znaki zapytania. Jeśli to zrobisz, sytuacja prawdopodobnie nieoczekiwanie się zepsuje.
  • Reszta Twojego skryptu nie zawiera niczego w tle. Jeśli to zrobisz lub później dodasz coś do skryptu, który zostanie wysłany w tle, ponieważ zapomniałeś, że nie możesz używać zadań w tle z powodu jego fragmentu, wszystko się zepsuje.

Inne przybliżenie, które nie ma tych wad, jest następujące:

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

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

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

    wait "${pids[@]}"
}

Zwróć uwagę, że ten można łatwo dostosować, aby sprawdzić również kod zakończenia każdego zadania po jego zakończeniu, aby można było ostrzec użytkownika, jeśli zadanie się nie powiedzie, lub ustawić kod zakończenia w scheduleAllzależności od liczby zadań, które zakończyły się niepowodzeniem, lub coś innego.

Problem z tym kodem jest taki, że:

  • Planuje cztery (w tym przypadku) zadania naraz, a następnie czeka na zakończenie wszystkich czterech. Niektóre mogą zostać wykonane wcześniej niż inne, co spowoduje, że kolejna partia czterech zadań będzie czekać, aż zostanie wykonana najdłuższa z poprzedniej.

Rozwiązanie, które rozwiązuje ten ostatni problem, musiałoby użyć kill -0do sondowania, czy któryś z procesów zniknął, zamiast waiti zaplanowania następnego zadania. Jednak wprowadza to mały nowy problem: między zakończeniem zadania a kill -0sprawdzeniem, czy się skończył , występuje wyścig . Jeśli zadanie się zakończyło, a inny proces w systemie zostanie uruchomiony w tym samym czasie, biorąc losowy PID, który jest taki sam, jak zadanie, które właśnie się zakończyło, kill -0nie zauważą, że praca została zakończona i wszystko znowu się zepsuje.

Idealne rozwiązanie nie jest możliwe w bash.


3

funkcja dla 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
}

za pomocą:

cat my_commands | parallel -j 4

Użycie make -jjest sprytne, ale bez żadnego wyjaśnienia i tej plamy kodu Awk tylko do zapisu, powstrzymuję się od głosowania za.
tripleee

2

Projekt, nad którym pracuję, używa polecenia wait do kontrolowania równoległych procesów powłoki (właściwie ksh). Aby rozwiać obawy dotyczące operacji we / wy, w nowoczesnym systemie operacyjnym możliwe jest, że wykonywanie równoległe faktycznie zwiększy wydajność. Jeśli wszystkie procesy odczytują te same bloki na dysku, tylko pierwszy proces będzie musiał trafić do fizycznego sprzętu. Inne procesy często będą w stanie pobrać blok z pamięci podręcznej dysku systemu operacyjnego w pamięci. Oczywiście odczyt z pamięci jest o kilka rzędów wielkości szybszy niż odczyt z dysku. Ponadto korzyść nie wymaga żadnych zmian w kodowaniu.


1

Może to być wystarczające do większości zastosowań, ale nie jest optymalne.

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

1

Oto jak udało mi się rozwiązać ten problem w skrypcie 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

1

Naprawdę późno na imprezę tutaj, ale oto inne rozwiązanie.

Wiele rozwiązań nie obsługuje spacji / znaków specjalnych w poleceniach, nie powoduje ciągłego działania N zadań, zjada procesora w zajętych pętlach lub polega na zewnętrznych zależnościach (np. GNU parallel).

Z inspiracji do obsługi procesów martwych / zombie , oto czyste rozwiązanie 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
}

I przykładowe użycie:

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

Wyjście:

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

Do obsługi danych wyjściowych dla poszczególnych procesów $$można użyć do zalogowania się do pliku, na przykład:

function job_done {
    cat "$1.log"
}

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

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

Wynik:

1 56871
2 56872

0

Możesz użyć prostej zagnieżdżonej pętli for (podstaw poniżej odpowiednie liczby całkowite dla N i M):

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

Spowoduje to wykonanie do_something N * M razy w M rundach, przy czym każda runda wykonuje równolegle N zadań. Możesz ustawić N równe liczbie posiadanych procesorów.


0

Moje rozwiązanie, aby zawsze utrzymywać określoną liczbę procesów, śledzić błędy i obsługiwać procesy ubnterruptible / zombie:

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
}

Stosowanie:

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"

-1

$ DOMAINS = "lista niektórych domen w poleceniach" dla foo in some-command do

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

Gotowe

Ndomeny =echo $DOMAINS |wc -w

for i w $ (seq 1 1 $ Ndomains) wykonaj echo "czekaj na $ {praca [$ i]}" czekaj "$ {praca [$ i]}" gotowe

w tej koncepcji będzie działać dla równoległości. Ważną rzeczą jest to, że ostatnia linia eval to „&”, co spowoduje umieszczenie poleceń w tle.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.