Pytanie Równoległy skrypt Bash z maksymalną liczbą procesów


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

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

do-something jest związany z procesorem i mam ładny, błyszczący, czterordzeniowy procesor. Chciałbym móc pobiec do 4 do-somethingjest od razu.

Naiwne podejście wydaje się być:

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

To się uruchomi wszystko  do-somethings od razu, ale jest kilka wady, głównie to zrobić - coś może mieć również znaczący I / O, który wykonuje wszystko od razu może trochę zwolnić. Innym problemem jest to, że ten blok kodu natychmiast powraca, więc nie ma możliwości wykonania innej pracy, gdy wszystkie do-somethings skończone.

Jak napisałbyś tę pętlę, więc zawsze jest X do-somethings działa od razu?


76
2017-09-01 16:47


pochodzenie


Jako sidenode, marzyłem o dodaniu opcji make-j do basha dla prymitywów. To nie działało zawsze, ale w niektórych prostych przypadkach, gdy wiesz, że ciało pętli zrobi coś unikalnego dla każdej iteracji, byłoby całkiem czysto, by powiedzieć "dla -j 4 ...". - unwind
Odsyłacz do stackoverflow.com/questions/1537956/... dla rozwiązania bash, które łagodzi problemy z wydajnością i pozwala grupom podprocesów być oddzielone. - paxdiablo
Poleciłbym moje rozwiązanie stackoverflow.com/a/28965927/340581 - Tuttle


Odpowiedzi:


W zależności od tego, co chcesz zrobić, xargs może również 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.

52
2018-05-19 07:50



Ta metoda, moim zdaniem, jest najbardziej eleganckim rozwiązaniem. Z wyjątkiem tego, że jestem paranoikiem, zawsze lubię używać find [...] -print0 i xargs -0. - amphetamachine
cpus=$(getconf _NPROCESSORS_ONLN) - mr.spuratic
Z podręcznika, dlaczego nie skorzystać --max-procs=0 uzyskać jak najwięcej procesów, jak to możliwe? - EverythingRightPlace
@EverythingRightPlace, pytanie wprost nie wymaga więcej procesów niż dostępne procesory. --max-procs=0 bardziej przypomina próbę pytającego (rozpocznij tyle procesów, ile argumentów). - Toby Speight


Z 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 rdzenia procesora na zdalnych komputerach - nawet jeśli mają one różną liczbę rdzeni:

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

Bardziej zaawansowany przykład: Tutaj znajduje się lista plików, które chcemy uruchomić my_script. Pliki mają rozszerzenie (może .jpeg). Chcemy, aby wyjście my_script było umieszczane obok plików w baseame.out (np. Foo.jpeg -> foo.out). Chcemy uruchomić mój skrypt raz dla każdego rdzenia, jaki ma komputer 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 my_script chcemy, aby foo.out został przeniesiony z powrotem, a następnie chcemy usunąć foo.jpeg i foo.out z komputera zdalnego:

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 mieszają się, 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


35
2018-06-10 01:37



Zauważ, że jest to bardzo przydatne przy korzystaniu z find polecenie generowania listy plików, ponieważ nie tylko zapobiega problemowi, gdy w nazwie pliku znajduje się spacja for i in ...; do ale znaleźć można również find -name \*.extension1 -or -name \*.extension2 który {.} GNU paralela radzi sobie bardzo ładnie. - Leo Izen
Plus 1 chociaż cat jest oczywiście bezużyteczny. - tripleee
@tripleee Re: Bezużyteczne wykorzystanie kota. Widzieć 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 jest niestety martwa, ale przekierowujący Iki powinien kontynuować pracę. - tripleee


maxjobs = 4
parallelize () {
        podczas gdy [$ # -gt 0]; zrobić
                jobcnt = (`jobs -p`)
                if [$ {# jobcnt [@]} -lt $ maxjobs]; następnie
                        zrób coś 1 $ i
                        przesunięcie
                jeszcze
                        spać 1
                fi
        Gotowe
        czekać
}

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

22
2017-09-01 18:00



Uświadom sobie, że jest trochę poważny underquoting dzieje się tutaj, więc wszelkie zadania, które wymagają spacji w argumentach, zawiodą; co więcej, skrypt ten pożera procesor twojego komputera, czekając na zakończenie niektórych zadań, jeśli więcej zadań jest wymaganych, niż pozwala na to maxjobs. - lhunath
Zauważ również, że to oznacza, że ​​twój skrypt nie robi nic innego, co zrobić z zadaniami; jeśli tak, to policzy również tych, którzy są w stosunku do maxjobów. - lhunath
Możesz użyć "jobs -pr", aby ograniczyć do uruchomionych zadań. - amphetamachine
Dodano komendę uśpienia, aby zapobiec powtarzaniu pętli while bez żadnego zerwania, podczas gdy ona czeka na już działające komendy do-coś, aby zakończyć. W przeciwnym razie ta pętla w istocie zajmie jeden z rdzeni procesora. To również rozwiązuje problem @lhatha. - euphoria83


Zamiast zwykłego basha użyj pliku Makefile, a następnie określ liczbę równoczesnych zadań make -jX gdzie X to liczba zadań do wykonania naraz.

Lub możesz użyć wait ("man wait"): uruchom kilka procesów potomnych, zadzwoń wait - zakończy się, gdy proces potomny zakończy się.

maxjobs = 10

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

job ( ){
...
}

Jeśli chcesz zapisać wynik pracy, przypisz wynik do zmiennej. Po wait po prostu sprawdź, co zawiera zmienna.


11
2017-09-01 16:50



Dzięki temu, mimo że kod nie jest gotowy, dostałem odpowiedź na problem, który mam w pracy. - gerikson
Jedynym problemem jest to, że jeśli zabijesz skrypt pierwszego planu (ten z pętlą), zadania, które były uruchomione, nie zostaną zabite razem - Girardi


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


8
2017-09-01 16:55





Oto alternatywne rozwiązanie, które można wstawić do .bashrc i użyć do codziennej jednej linijki:

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

Aby z niego skorzystać, wystarczy postawić & 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 przyjemniej korzystać wait zamiast intensywnego czekania na wyjście jobs -p, ale nie wydaje się oczywistym rozwiązaniem, aby poczekać, aż któreś z podanych zadań zostanie zakończone zamiast wszystkich.


8
2018-05-19 03:40





Robiąc to w prawo bash jest prawdopodobnie niemożliwe, możesz zrobić pół-prawo dość łatwo. bstark dał sprawiedliwe przybliżenie prawa, ale ma następujące wady:

  • Podział tekstu: Nie można przekazać do niego żadnych zadań, które wykorzystują w swoich argumentach dowolny z następujących znaków: spacje, tabulacje, znaki nowej linii, gwiazdki, znaki zapytania. Jeśli to zrobisz, wszystko się zepsuje, być może niespodziewanie.
  • Opiera się on na pozostałej części skryptu, aby nie tworzyć tła. 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 urywka, 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[@]}"
}

Zauważ, że ten można łatwo dostosować, aby również sprawdzić kod zakończenia każdego zadania po jego zakończeniu, aby ostrzec użytkownika, jeśli zadanie nie działa lub ustawić kod zakończenia dla scheduleAll w zależności od liczby zleceń, które się nie powiodły lub coś takiego.

Problem z tym kodem polega na tym, że:

  • Zaplanowano cztery (w tym przypadku) zadania na raz, a następnie czeka na zakończenie wszystkich czterech. Niektóre mogą być wykonane wcześniej niż inne, co spowoduje, że następna partia czterech zadań będzie czekała, aż zrobi się najdłuższa z poprzedniej partii.

Potrzebne byłoby rozwiązanie, które zajmie się tym ostatnim problemem kill -0 sondować, czy któryś z procesów zniknął zamiast wait i zaplanuj następną pracę. Wprowadza to jednak niewielki nowy problem: masz stan wyścigu między zakończeniem zadania a kill -0 sprawdzanie, czy to się skończyło. Jeśli zadanie zostanie zakończone, a inny proces w systemie zostanie uruchomiony w tym samym czasie, biorąc losowy PID, który jest przypadkiem właśnie zakończonego zadania, kill -0 nie zauważy, że twoja praca skończyła się i wszystko znów się zepsuje.

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


6
2018-05-19 07:26





Jeśli znasz dobrze make polecenie, 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 produkuje * .output, możesz użyć makefile

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

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

all: $ (WYJŚCIE)

i po prostu uciekaj

make -j <NUMBER>

do równoległego uruchamiania najwyżej NUMBER poleceń.


5
2018-05-21 20:33





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

3
2018-02-22 10:14



Sposób użycia make -j jest sprytny, ale bez żadnego wyjaśnienia i tego blobu kodu Awk, który tylko pisze, powstrzymuję się od przegrywania. - tripleee