Параллельные процессы: добавление выходных данных в массив в скрипте bash

У меня есть цикл, в котором вызывается task функции. Каждый вызов функции возвращает строку, которая добавляется в массив. Я хотел бы распараллелить это для цикла. Я пытался использовать & но это не похоже на работу.

Здесь код не распараллелен.

 task (){ sleep 1;echo "hello $1"; } arr=() for i in {1..3}; do arr+=("$(task $i)") done for i in "${arr[@]}"; do echo "$ix"; done 

Выход:

 hello 1 x hello 2 x hello 3 x 

Большой! Но теперь, когда я пытаюсь распараллелить это с

 [...] for i in {1..3}; do arr+=("$(task $i)")& done wait [...] 

выход пуст.

ОБНОВЛЕНИЕ № 1

Относительно функции task :

  • Задача функции занимает некоторое время, а затем выводит одну строку. После того, как все строки собраны, другой цикл for будет проходить по строкам и выполнять другие задачи.
  • Порядок не имеет значения. Выходная строка может состоять из одной строки, возможно, с несколькими словами, разделенными пробелом.

Вы не можете отправить назначение фону, так как фоновый процесс является форком оболочки, а изменения в переменной не видны в основной оболочке.

Но вы можете выполнять несколько задач параллельно, выводить их все в канал и затем читать все, что выходит. Или, на самом деле, используйте подстановку процессов , чтобы избежать проблем с командами в конвейере, выполняемых в подоболочке (см. Почему моя переменная локальна в одном цикле «пока читается», а не в другом, на первый взгляд, похожем цикле? )

Пока выходные данные представляют собой отдельные строки, написанные атомарно, они не будут смешиваться, но могут быть переупорядочены:

 $ task() { sleep 1; echo "$1"; } $ time while read -r line; do arr+=("$line"); done < <(for x in 1 2 3 ; do task "$x" & done) real 0m1.006s $ declare -p arr declare -a arr=([0]="2" [1]="1" [2]="3") 

Выше будет запускать все задачи одновременно. Также есть GNU параллельно (и -P в GNU xargs), которое предназначено именно для параллельного выполнения задач и будет запускать только несколько одновременно. Параллель также буферизирует выходные данные задач, поэтому вы не получите смешанных данных, даже если задача записывает строки по частям.

 $ mapfile -t arr < <(parallel -j4 bash ./task.sh ::: {a,b,c}) $ declare -p arr declare -a arr=([0]="a" [1]="b" [2]="c") 

( mapfile Bash здесь читает входные строки в массив, аналогично циклу while read .. arr+=() выше.)

Выполнение внешнего сценария, как описано выше, является простым, но вы также можете запустить его экспортированную функцию, хотя, конечно, все задачи выполняются в независимых копиях оболочки, поэтому у них будут свои копии каждой переменной и т. Д.

 $ export -f task $ mapfile -t arr < <(parallel task ::: {a,b,c}) 

Вышеприведенный пример держал в порядке a , b и c , но это совпадение. Используйте parallel -k , чтобы убедиться, что выходные данные сохранены в порядке.

Немного наивный, но надежный способ сделать это, переносимый через борновские оболочки:

 #!/bin/sh task () { tid="$1" printf 'tid %d: Running...\n' "$tid" sleep "$(( RANDOM % 5 ))" printf 'tid %d: Done.\n' "$tid" } ntasks=10 tid=0 while [ "$tid" -ne "$ntasks" ]; do tid=$(( tid + 1 )) printf 'main: Starting task with tid=%d\n' "$tid" task "$tid" >"output.$tid" 2>&1 & done wait tid=0 while [ "$tid" -ne "$ntasks" ]; do tid=$(( tid + 1 )) printf 'main: Processing output from task with tid=%d\n' "$tid" # do something with "output.$tid" done 

Это порождает задачи в первом цикле, затем ожидает их завершения, прежде чем обрабатывать выходные данные во втором цикле. Это подойдет, если задачи дают много данных.

Чтобы ограничить количество выполняемых задач максимум 4, начальный цикл может быть изменен на

 tid=0 while [ "$tid" -ne "$ntasks" ]; do tid=$(( tid + 1 )) printf 'main: Starting task with tid=%d\n' "$tid" task "$tid" >"output.$tid" 2>&1 & if [ "$(( tid % 4 ))" -eq 0 ]; then wait fi done 

Вы ищете parset (часть GNU Parallel с 20170422) или env_parset (доступен с 20171222):

 # If you have not run: # env_parallel --install # and logged in again, then you can instead run this to activate (env_)parset: . `which env_parallel.bash` task (){ echo "hello $1" sleep 1.$1 perl -e 'print "binary\001\002\n"' sleep 1.$1 echo output of parallel jobs do not mix } env_parset arr task ::: {1..3} env_parset a,b,c task ::: {1..3} echo "${arr[1]}" | xxd echo "$b" | xxd 

parset поддерживается в Bash / Ksh / Zsh (включая массивы), ash / dash (без массивов).