Распараллелить Bash FOR Loop

109

Я пытался распараллелить следующий скрипт, в частности, каждый из трех экземпляров цикла FOR, используя GNU Parallel, но не смог. Четыре команды, содержащиеся в цикле 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команду в конце, чтобы главный сценарий не завершился, пока не выполнятся все фоновые задания.
Псуси
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 
PSkocik
источник
4
Строка с waitним в основном позволяет запускать все процессы, пока он не попадет в nthпроцесс, а затем не дождется завершения всех остальных, верно?
naught101
Если iноль, звоните, подождите. Инкремент iпосле нулевого теста.
PSkocik
2
@ naught101 Да. waitбез аргументов ждет всех детей. Это делает его немного расточительным. Подход на основе конвейерного семафора дает вам более быстрый параллелизм (я уже использовал это в пользовательской системе -nt-ot
компоновки на
1
@ BeowulfNode42 Вам не нужно выходить. Статус возврата задачи не будет вредить согласованности семафора, пока статус (или что-то с этой длиной байта) записывается обратно в fifo после завершения / сбоя процесса задачи.
PSkocik
1
К сведению, 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Выглядит немного склонный к конфликтам , если он работает параллельно ...

frostschutz
источник
2
Это работает отлично, а также. Вы правы, я должен был бы перейти rm *.matна что-то подобное, rm $run".mat"чтобы заставить его работать без вмешательства одного процесса в другой. Спасибо .
Ravnoor S Gill
@RavnoorSGill Добро пожаловать в Stack Exchange! Если этот ответ решил вашу проблему, отметьте его как принятый , поставив галочку рядом с ним.
Жиль
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 --wait сообщает, что нужно дождаться завершения всех итераций цикла for, прежде чем выполнять последовательные строки кода.

Примечание: вам потребуется «параллель» из параллельного проекта GNU (sudo apt-get install parallel).

лев
источник
1
можно ли пройти за 60? мой выдает ошибку, говоря, что недостаточно файловых дескрипторов.
Чови
Если это приводит к синтаксической ошибке из-за скобок для всех, посмотрите на ответ moritzschaefer.
Николай
10

Один очень простой способ, которым я часто пользуюсь:

cat "args" | xargs -P $NUM_PARALLEL command

Это запустит команду, передавая в каждой строке файла «args» параллельно, запустив не более $ NUM_PARALLEL одновременно.

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

EyeApps LLC
источник
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 к числу ядер ЦП. Запустите до этого много рабочих мест параллельно. Для ресурсоемких заданий -j +0 полезен, так как он будет одновременно запускать задания количества процессорных ядер.

-j -N Вычесть N из числа ядер ЦП. Запустите до этого много рабочих мест параллельно. Если оцененное число меньше 1, то будет использоваться 1. Смотрите также --use-cpus-вместо-cores.

moritzschaefer
источник
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
geekley
источник
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
Zhro
источник