Паралелізуйте цикл Bash FOR


109

Я намагався паралелізувати наступний скрипт, зокрема кожен із трьох екземплярів циклу FOR, використовуючи GNU Parallel, але не зміг. 4 команди, що містяться в циклі FOR, виконуються послідовно, кожен цикл займає близько 10 хвилин.

#!/bin/bash

kar='KAR5'
runList='run2 run3 run4'
mkdir normFunc
for run in $runList
do 
  fsl5.0-flirt -in $kar"deformed.nii.gz" -ref normtemp.nii.gz -omat $run".norm1.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
  fsl5.0-flirt -in $run".poststats.nii.gz" -ref $kar"deformed.nii.gz" -omat $run".norm2.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
  fsl5.0-convert_xfm -concat $run".norm1.mat" -omat $run".norm.mat" $run".norm2.mat"
  fsl5.0-flirt -in $run".poststats.nii.gz" -ref normtemp.nii.gz -out $PWD/normFunc/$run".norm.nii.gz" -applyxfm -init $run".norm.mat" -interp trilinear

  rm -f *.mat
done

Відповіді:


94

Чому ти просто не розщедриш їх (ака. Тло)?

foo () {
    local run=$1
    fsl5.0-flirt -in $kar"deformed.nii.gz" -ref normtemp.nii.gz -omat $run".norm1.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref $kar"deformed.nii.gz" -omat $run".norm2.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-convert_xfm -concat $run".norm1.mat" -omat $run".norm.mat" $run".norm2.mat"
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref normtemp.nii.gz -out $PWD/normFunc/$run".norm.nii.gz" -applyxfm -init $run".norm.mat" -interp trilinear
}

for run in $runList; do foo "$run" & done

Якщо це не зрозуміло, значна частина тут:

for run in $runList; do foo "$run" & done
                                   ^

Причиняючи функцію виконання у роздвоєній оболонці на задньому плані. Це паралельно.


6
Це спрацювало як шарм. Дякую. Така проста реалізація (Змушує мене зараз почуватися дурною!).
— Ravnoor S Gill

8
У випадку, якщо у мене було 8 файлів для паралельного запуску, але лише 4 ядра, чи можна їх інтегрувати в таке налаштування або для цього потрібен планувальник роботи?
— Ravnoor S Gill

6
Це насправді не має значення в цьому контексті; нормально, щоб система мала більш активні процеси, ніж ядра. Якщо у вас багато коротких завдань , в ідеалі ви будете годувати чергу, обслуговувану номером або робочими потоками <кількість ядер. Я не знаю, як часто це робиться на сценаріях оболонок (у такому випадку вони не будуть нитками, вони будуть незалежними процесами), але з відносно мало довгими завданнями це буде безглуздо. Про них подбає планувальник ОС.
— золотинки

17
Ви також можете додати waitкоманду в кінці, щоб головний скрипт не вийшов, поки не виконуються всі фонові завдання.
— psusi

1
Також я б корисно обмежити кількість одночасних процесів: кожен мій процес використовує 100% часу ядра протягом приблизно 25 хвилин. Це на спільному сервері з 16 ядрами, де багато людей працюють за роботою. Мені потрібно запустити 23 копії сценарію. Якщо я запускаю їх одночасно, то я болотую сервер і роблю його корисним для всіх інших протягом години-двох (навантаження йде до 30, все інше сповільнюється). Я думаю, що це можна було б зробити nice, але тоді я не знаю, чи колись це закінчиться ..
— naught101

150

Зразок завдання

task(){
   sleep 0.5; echo "$1";
}

Послідовні прогони

for thing in a b c d e f g; do 
   task "$thing"
done

Паралельні біги

for thing in a b c d e f g; do 
  task "$thing" &
done

Паралельні запуски в N-процесових партіях

N=4
(
for thing in a b c d e f g; do 
   ((i=i%N)); ((i++==0)) && wait
   task "$thing" & 
done
)

Також можна використовувати FIFO в якості семафорів і використовувати їх для того, щоб нові процеси виникали якнайшвидше і що одночасно запускається не більше N процесів. Але для цього потрібно більше коду.

N процесів із семафором на основі FIFO:

open_sem(){
    mkfifo pipe-$$
    exec 3<>pipe-$$
    rm pipe-$$
    local i=$1
    for((;i>0;i--)); do
        printf %s 000 >&3
    done
}
run_with_lock(){
    local x
    read -u 3 -n 3 x && ((0==x)) || exit $x
    (
     ( "$@"; )
    printf '%.3d' $? >&3
    )&
}

N=4
open_sem $N
for thing in {a..g}; do
    run_with_lock task $thing
done 

4
Рядок з waitним в основному дозволяє виконувати всі процеси, поки він не потрапить у nthпроцес, а потім чекає, поки всі інші закінчать роботу, чи не так?
— naught101

Якщо iнуль, дзвоніть, зачекайте. Збільшення iпісля нульового тесту.
— PSkocik

2
@ naught101 Так. waitбез / арг не чекає на всіх дітей. Це робить його трохи марнотратним. Підхід на основі семафору надає вам більш вільну конкурентоспроможність (я використовую це в спеціальній системі побудови на оболонці разом із -nt/ -otуспішно перевіряє деякий час)
— PSkocik

1
@ BeowulfNode42 Вам не доведеться виходити. Статус повернення задачі не зашкодить послідовності семафору, поки статус (або щось з такою довжиною байта) записується на фіфо після завершення / збоїв процесу завдання.
— PSkocik

1
Команді FYI mkfifo pipe-$$потрібен відповідний доступ для запису до поточного каталогу. Тому я вважаю за краще вказати повний шлях, наприклад, /tmp/pipe-$$оскільки він, швидше за все, має доступ для запису для поточного користувача, а не покладатися на те, що є поточним каталогом. Так замінити всі 3 випадки pipe-$$.
— BeowulfNode42

65
for stuff in things
do
( something
  with
  stuff ) &
done
wait # for all the something with stuff

Чи справді це працює, залежить від ваших команд; Я з ними не знайомий. Вигляд rm *.matтрохи схильний до конфліктів, якщо він працює паралельно ...


2
Це також ідеально. Ви маєте рацію, мені доведеться змінити rm *.matщось на кшталт, rm $run".mat"щоб змусити його працювати без того, щоб один процес заважав іншому. Дякую .
— Ravnoor S Gill

@RavnoorSGill Ласкаво просимо до обміну стеками! Якщо ця відповідь вирішила вашу проблему, позначте її як прийняту , поставивши галочку біля неї.
— Жиль

7
+1 за wait, про що я забув.
— золотинки

5
Якщо є багато «речей», чи не це запустить багато процесів? Було б краще запустити лише розумну кількість процесів одночасно, правда?
— Девід Дорія

1
Дуже корисна порада! Як налаштувати кількість потоків у цьому випадку?
— Дадун Чжан

30
for stuff in things
do
sem -j+0 ( something
  with
  stuff )
done
sem --wait

Це використовуватиме семафори, паралелізуючи стільки ітерацій, скільки кількість наявних ядер (-j +0 означає, що ви будете паралелізувати N + 0 завданням , де N - кількість доступних ядер ).

sem --очекає, чекати, поки всі ітерації циклу for завершаться, перш ніж виконувати послідовні рядки коду.

Примітка: вам знадобиться "паралель" від паралельного проекту GNU (sudo apt-get install paralle).


1
чи можна пройти повз 60? моє видає помилку, кажучи про недостатню кількість дескрипторів файлів.
— chovy

Якщо це кидає синтаксичну помилку через дужки для когось, подивіться на відповідь moritzschaefer.
— Миколай

10

Один дуже простий спосіб, який я часто використовую:

cat "args" | xargs -P $NUM_PARALLEL command

Це запустить команду, паралельно проходячи у кожному рядку файла "args", паралельно, запустивши не більше $ NUM_PARALLEL одночасно.

Ви також можете розглянути параметр -I для xargs, якщо вам потрібно замінити вхідні аргументи в різних місцях.


6

Здається, завдання FSL залежать від іншого, тому 4 завдання не можна виконувати паралельно. Пробіги, однак, можна виконувати паралельно.

Зробіть функцію bash, що працює в одному циклі, і виконайте цю функцію паралельно:

#!/bin/bash

myfunc() {
    run=$1
    kar='KAR5'
    mkdir normFunc
    fsl5.0-flirt -in $kar"deformed.nii.gz" -ref normtemp.nii.gz -omat $run".norm1.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref $kar"deformed.nii.gz" -omat $run".norm2.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-convert_xfm -concat $run".norm1.mat" -omat $run".norm.mat" $run".norm2.mat"
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref normtemp.nii.gz -out $PWD/normFunc/$run".norm.nii.gz" -applyxfm -init $run".norm.mat" -interp trilinear
}

export -f myfunc
parallel myfunc ::: run2 run3 run4

Щоб дізнатися більше, перегляньте вступні відео: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1 та проведіть годину, прогулюючись підручником http://www.gnu.org/software/parallel/parallel_tutorial.html Ваша команда лінія полюбить вас за це.


Якщо ви використовуєте не-bash оболонку, вам знадобиться також export SHELL=/bin/bashперед паралельним запуском. Інакше ви отримаєте помилку на кшталт:Unknown command 'myfunc arg'
— AndrewHarvey

1
@AndrewHarvey: чи не для чого це шебанг?
— naught101

5

Паралельне виконання в максимальному паралельному процесі N

#!/bin/bash

N=4

for i in {a..z}; do
    (
        # .. do your stuff here
        echo "starting task $i.."
        sleep $(( (RANDOM % 3) + 1))
    ) &

    # allow only to execute $N jobs in parallel
    if [[ $(jobs -r -p | wc -l) -gt $N ]]; then
        # wait only for first job
        wait -n
    fi

done

# wait for pending jobs
wait

echo "all done"

3

Мені дуже подобається відповідь від @lev, оскільки вона забезпечує контроль над максимальною кількістю процесів дуже просто. Однак, як описано в посібнику , sem не працює з дужками.

for stuff in things
do
sem -j +0 "something; \
  with; \
  stuff"
done
sem --wait

Робить роботу.

-j + N Додайте N до числа ядер CPU. Виконайте до цього багато завдань паралельно. Для обчислювальних завдань -j +0 корисний, оскільки він буде одночасно виконувати завдання з кількістю ядер CPU.

-j -N Відняти N від кількості ядер CPU. Виконайте до цього багато завдань паралельно. Якщо оцінене число менше 1, то буде використано 1. Дивіться також - використання-cpus-замість-ядер.


1

У моєму випадку я не можу використовувати семафор (я в git-bash в Windows), тому я придумав загальний спосіб розділити завдання серед N працівників, перш ніж вони розпочнуться.

Це добре працює, якщо завдання займають приблизно стільки ж часу. Недоліком є ​​те, що якщо одному з робітників потрібен довгий час, щоб виконати свою частину роботи, інші, які вже закінчили, не допоможуть.

Розподіл роботи серед N працівників (1 на ядро)

# array of assets, assuming at least 1 item exists
listAssets=( {a..z} ) # example: a b c d .. z
# listAssets=( ~/"path with spaces/"*.txt ) # could be file paths

# replace with your task
task() { # $1 = idWorker, $2 = asset
  echo "Worker $1: Asset '$2' START!"
  # simulating a task that randomly takes 3-6 seconds
  sleep $(( ($RANDOM % 4) + 3 ))
  echo "    Worker $1: Asset '$2' OK!"
}

nVirtualCores=$(nproc --all)
nWorkers=$(( $nVirtualCores * 1 )) # I want 1 process per core

worker() { # $1 = idWorker
  echo "Worker $1 GO!"
  idAsset=0
  for asset in "${listAssets[@]}"; do
    # split assets among workers (using modulo); each worker will go through
    # the list and select the asset only if it belongs to that worker
    (( idAsset % nWorkers == $1 )) && task $1 "$asset"
    (( idAsset++ ))
  done
  echo "    Worker $1 ALL DONE!"
}

for (( idWorker=0; idWorker<nWorkers; idWorker++ )); do
  # start workers in parallel, use 1 process for each
  worker $idWorker &
done
wait # until all workers are done

0

У мене виникли проблеми з @PSkocikрішенням Росії. У моїй системі немає GNU Parallel як пакет, і він semкинув виняток, коли я створив і запустив його вручну. Потім я спробував приклад семафору FIFO, який також кинув деякі інші помилки щодо спілкування.

@eyeApps запропонував xargs, але я не знав, як змусити його працювати зі своїм складним випадком використання (приклади будуть вітатися).

Ось моє рішення для паралельних завдань, які обробляють до Nзавдань одночасно, як налаштовано _jobs_set_max_parallel:

_lib_jobs.sh:

function _jobs_get_count_e {
   jobs -r | wc -l | tr -d " "
}

function _jobs_set_max_parallel {
   g_jobs_max_jobs=$1
}

function _jobs_get_max_parallel_e {
   [[ $g_jobs_max_jobs ]] && {
      echo $g_jobs_max_jobs

      echo 0
   }

   echo 1
}

function _jobs_is_parallel_available_r() {
   (( $(_jobs_get_count_e) < $g_jobs_max_jobs )) &&
      return 0

   return 1
}

function _jobs_wait_parallel() {
   # Sleep between available jobs
   while true; do
      _jobs_is_parallel_available_r &&
         break

      sleep 0.1s
   done
}

function _jobs_wait() {
   wait
}

Приклад використання:

#!/bin/bash

source "_lib_jobs.sh"

_jobs_set_max_parallel 3

# Run 10 jobs in parallel with varying amounts of work
for a in {1..10}; do
   _jobs_wait_parallel

   # Sleep between 1-2 seconds to simulate busy work
   sleep_delay=$(echo "scale=1; $(shuf -i 10-20 -n 1)/10" | bc -l)

   ( ### ASYNC
   echo $a
   sleep ${sleep_delay}s
   ) &
done

# Visualize jobs
while true; do
   n_jobs=$(_jobs_get_count_e)

   [[ $n_jobs = 0 ]] &&
      break

   sleep 0.1s
done
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.