Как вы запускаете несколько программ параллельно с bash-скриптом?
Я пытаюсь написать .sh файл, который запускает много программ одновременно
Я пробовал это
prog1
prog2
Но это запускает prog1, затем ждет, пока prog1 заканчивается, а затем запускает prog2...
Так как я могу запустить их параллельно?
21 ответ
Как насчет:
prog1 & prog2 && fg
Это будет:
- Начните
prog1
, - Отправьте его на задний план, но продолжайте печатать вывод.
- Начните
prog2
и держите его на переднем плане, чтобы вы могли закрыть егоctrl-c
, - Когда вы закрываете
prog2
, ты вернешься кprog1
передний план, так что вы также можете закрыть его сctrl-c
,
Если вы хотите иметь возможность легко запускать и убивать несколько процессов с ctrl-c
это мой любимый метод: порождать несколько фоновых процессов в (…)
подоболочка и ловушка SIGINT
выполнить kill 0
, который убьет все, что появилось в группе subshell:
(trap 'kill 0' SIGINT; prog1 & prog2 & prog3)
Вы можете иметь сложные структуры выполнения процесса, и все будет закрыто с помощью одного ctrl-c
(просто убедитесь, что последний процесс запущен на переднем плане):
(trap 'kill 0' SIGINT; prog1.1 && prog1.2 & (prog2.1 | prog2.2 || prog2.3) & prog 1.3)
Ты можешь использовать wait
:
some_command &
P1=$!
other_command &
P2=$!
wait $P1 $P2
Он присваивает переменные PID фоновой программы переменным ($!
это последний запущенный процесс ' PID), затем wait
Команда ждет их. Это хорошо, потому что если вы убьете скрипт, он тоже убьет процессы!
С помощью GNU Parallel http://www.gnu.org/software/parallel/ это так же просто, как:
(echo prog1; echo prog2) | parallel
Или, если вы предпочитаете:
parallel ::: prog1 prog2
Учить больше:
- Посмотрите вступительное видео для быстрого ознакомления: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1
- Пройдите учебник (man parallel_tutorial). Ваша командная строка будет любить вас за это.
xargs -P <n>
позволяет бежать <n>
команды параллельно.
В то время как -P
это нестандартная опция, которую поддерживают как GNU (Linux), так и реализации macOS/BSD.
Следующий пример:
- запускает не более 3 команд одновременно,
- с дополнительными командами, запускающимися только после завершения ранее запущенного процесса.
time xargs -P 3 -I {} sh -c 'eval "$1"' - {} <<'EOF'
sleep 1; echo 1
sleep 2; echo 2
sleep 3; echo 3
echo 4
EOF
Вывод выглядит примерно так:
1 # output from 1st command
4 # output from *last* command, which started as soon as the count dropped below 3
2 # output from 2nd command
3 # output from 3rd command
real 0m3.012s
user 0m0.011s
sys 0m0.008s
Время показывает, что команды выполнялись параллельно (последняя команда была запущена только после того, как завершилась первая из исходных 3, но была выполнена очень быстро).
xargs
Сама команда не вернется, пока все команды не будут завершены, но вы можете выполнить ее в фоновом режиме, завершив ее оператором управления &
а затем с помощью wait
встроенный, чтобы ждать всего xargs
Команда, чтобы закончить.
{
xargs -P 3 -I {} sh -c 'eval "$1"' - {} <<'EOF'
sleep 1; echo 1
sleep 2; echo 2
sleep 3; echo 3
echo 4
EOF
} &
# Script execution continues here while `xargs` is running
# in the background.
echo "Waiting for commands to finish..."
# Wait for `xargs` to finish, via special variable $!, which contains
# the PID of the most recently started background process.
wait $!
Замечания:
BSD / MacOS
xargs
требует, чтобы вы указали количество команд для параллельного запуска, тогда как GNUxargs
позволяет указать-P 0
запустить как можно больше параллельно.Выходные данные параллельных процессов поступают по мере их генерирования, поэтому они будут непредсказуемо чередоваться.
- GNU
parallel
Как упоминалось в ответе Оле (не входит в стандартную комплектацию большинства платформ), удобно сериализует (группирует) вывод для каждого процесса и предлагает множество более сложных функций.
- GNU
Вот функция, которую я использую для параллельного запуска при максимальном n-процессе (в примере n=4):
max_children=4
function parallel {
local time1=$(date +"%H:%M:%S")
local time2=""
# for the sake of the example, I'm using $2 as a description, you may be interested in other description
echo "starting $2 ($time1)..."
"$@" && time2=$(date +"%H:%M:%S") && echo "finishing $2 ($time1 -- $time2)..." &
local my_pid=$$
local children=$(ps -eo ppid | grep -w $my_pid | wc -w)
children=$((children-1))
if [[ $children -ge $max_children ]]; then
wait -n
fi
}
parallel sleep 5
parallel sleep 6
parallel sleep 7
parallel sleep 8
parallel sleep 9
wait
Если для max_children задано количество ядер, эта функция будет пытаться избежать незанятых ядер.
#!/bin/bash
prog1 & 2> .errorprog1.log; prog2 & 2> .errorprog2.log
Перенаправлять ошибки в отдельные журналы.
Это прекрасно работает для меня ( найдено здесь ):
sh -c 'command1 & command2 & command3 & wait'
Он выводит все журналы каждой команды, перемешанные (это то, что я хотел), и все убиваются с помощью ctrl +c.
Существует очень полезная программа, которая вызывает nohup.
nohup - run a command immune to hangups, with output to a non-tty
Недавно у меня была похожая ситуация, когда мне нужно было запускать несколько программ одновременно, перенаправлять их выходные данные в отдельные файлы журнала и ждать, пока они закончат, и в итоге я получил нечто подобное:
#!/bin/bash
# Add the full path processes to run to the array
PROCESSES_TO_RUN=("/home/joao/Code/test/prog_1/prog1" \
"/home/joao/Code/test/prog_2/prog2")
# You can keep adding processes to the array...
for i in ${PROCESSES_TO_RUN[@]}; do
${i%/*}/./${i##*/} > ${i}.log 2>&1 &
# ${i%/*} -> Get folder name until the /
# ${i##*/} -> Get the filename after the /
done
# Wait for the processes to finish
wait
Источник: http://joaoperibeiro.com/execute-multiple-programs-and-redirect-their-outputs-linux/
Вы можете попробовать ppss. ppss довольно мощный - вы даже можете создать мини-кластер. xargs -P также может быть полезен, если вам нужно выполнить партию смущающей параллельной обработки.
Менеджер процесса порождения
Конечно, технически это процессы, и эта программа должна называться диспетчером порождения процессов, но это только из-за того, что BASH работает, когда он разветвляется, используя амперсанд, он использует системный вызов fork() или, возможно, clone(). который клонируется в отдельное пространство памяти, а не что-то вроде pthread_create(), который бы разделял память. Если бы BASH поддерживал последнее, каждая "последовательность выполнения" работала бы одинаково и могла бы быть названа традиционными потоками, в то же время получая более эффективный объем памяти. Функционально, однако, он работает так же, хотя и немного сложнее, поскольку переменные GLOBAL недоступны в каждом рабочем клоне, поэтому для управления критическими секциями используется файл межпроцессного взаимодействия и элементарный семафор стада. Формирование от BASH, конечно, является основным ответом здесь, но я чувствую, что люди знают это, но действительно хотят управлять тем, что порождено, а не просто раскошелиться и забыть это. Это демонстрирует способ управления до 200 экземплярами разветвленных процессов, каждый из которых обращается к одному ресурсу. Ясно, что это излишне, но мне понравилось писать, поэтому я продолжал. Увеличьте размер вашего терминала соответственно. Я надеюсь, что вы найдете это полезным.
ME=$(basename $0)
IPC="/tmp/$ME.ipc" #interprocess communication file (global thread accounting stats)
DBG=/tmp/$ME.log
echo 0 > $IPC #initalize counter
F1=thread
SPAWNED=0
COMPLETE=0
SPAWN=1000 #number of jobs to process
SPEEDFACTOR=1 #dynamically compensates for execution time
THREADLIMIT=50 #maximum concurrent threads
TPS=1 #threads per second delay
THREADCOUNT=0 #number of running threads
SCALE="scale=5" #controls bc's precision
START=$(date +%s) #whence we began
MAXTHREADDUR=6 #maximum thread life span - demo mode
LOWER=$[$THREADLIMIT*100*90/10000] #90% worker utilization threshold
UPPER=$[$THREADLIMIT*100*95/10000] #95% worker utilization threshold
DELTA=10 #initial percent speed change
threadspeed() #dynamically adjust spawn rate based on worker utilization
{
#vaguely assumes thread execution average will be consistent
THREADCOUNT=$(threadcount)
if [ $THREADCOUNT -ge $LOWER ] && [ $THREADCOUNT -le $UPPER ] ;then
echo SPEED HOLD >> $DBG
return
elif [ $THREADCOUNT -lt $LOWER ] ;then
#if maxthread is free speed up
SPEEDFACTOR=$(echo "$SCALE;$SPEEDFACTOR*(1-($DELTA/100))"|bc)
echo SPEED UP $DELTA%>> $DBG
elif [ $THREADCOUNT -gt $UPPER ];then
#if maxthread is active then slow down
SPEEDFACTOR=$(echo "$SCALE;$SPEEDFACTOR*(1+($DELTA/100))"|bc)
DELTA=1 #begin fine grain control
echo SLOW DOWN $DELTA%>> $DBG
fi
echo SPEEDFACTOR $SPEEDFACTOR >> $DBG
#average thread duration (total elapsed time / number of threads completed)
#if threads completed is zero (less than 100), default to maxdelay/2 maxthreads
COMPLETE=$(cat $IPC)
if [ -z $COMPLETE ];then
echo BAD IPC READ ============================================== >> $DBG
return
fi
#echo Threads COMPLETE $COMPLETE >> $DBG
if [ $COMPLETE -lt 100 ];then
AVGTHREAD=$(echo "$SCALE;$MAXTHREADDUR/2"|bc)
else
ELAPSED=$[$(date +%s)-$START]
#echo Elapsed Time $ELAPSED >> $DBG
AVGTHREAD=$(echo "$SCALE;$ELAPSED/$COMPLETE*$THREADLIMIT"|bc)
fi
echo AVGTHREAD Duration is $AVGTHREAD >> $DBG
#calculate timing to achieve spawning each workers fast enough
# to utilize threadlimit - average time it takes to complete one thread / max number of threads
TPS=$(echo "$SCALE;($AVGTHREAD/$THREADLIMIT)*$SPEEDFACTOR"|bc)
#TPS=$(echo "$SCALE;$AVGTHREAD/$THREADLIMIT"|bc) # maintains pretty good
#echo TPS $TPS >> $DBG
}
function plot()
{
echo -en \\033[${2}\;${1}H
if [ -n "$3" ];then
if [[ $4 = "good" ]];then
echo -en "\\033[1;32m"
elif [[ $4 = "warn" ]];then
echo -en "\\033[1;33m"
elif [[ $4 = "fail" ]];then
echo -en "\\033[1;31m"
elif [[ $4 = "crit" ]];then
echo -en "\\033[1;31;4m"
fi
fi
echo -n "$3"
echo -en "\\033[0;39m"
}
trackthread() #displays thread status
{
WORKERID=$1
THREADID=$2
ACTION=$3 #setactive | setfree | update
AGE=$4
TS=$(date +%s)
COL=$[(($WORKERID-1)/50)*40]
ROW=$[(($WORKERID-1)%50)+1]
case $ACTION in
"setactive" )
touch /tmp/$ME.$F1$WORKERID #redundant - see main loop
#echo created file $ME.$F1$WORKERID >> $DBG
plot $COL $ROW "Worker$WORKERID: ACTIVE-TID:$THREADID INIT " good
;;
"update" )
plot $COL $ROW "Worker$WORKERID: ACTIVE-TID:$THREADID AGE:$AGE" warn
;;
"setfree" )
plot $COL $ROW "Worker$WORKERID: FREE " fail
rm /tmp/$ME.$F1$WORKERID
;;
* )
;;
esac
}
getfreeworkerid()
{
for i in $(seq 1 $[$THREADLIMIT+1])
do
if [ ! -e /tmp/$ME.$F1$i ];then
#echo "getfreeworkerid returned $i" >> $DBG
break
fi
done
if [ $i -eq $[$THREADLIMIT+1] ];then
#echo "no free threads" >> $DBG
echo 0
#exit
else
echo $i
fi
}
updateIPC()
{
COMPLETE=$(cat $IPC) #read IPC
COMPLETE=$[$COMPLETE+1] #increment IPC
echo $COMPLETE > $IPC #write back to IPC
}
worker()
{
WORKERID=$1
THREADID=$2
#echo "new worker WORKERID:$WORKERID THREADID:$THREADID" >> $DBG
#accessing common terminal requires critical blocking section
(flock -x -w 10 201
trackthread $WORKERID $THREADID setactive
)201>/tmp/$ME.lock
let "RND = $RANDOM % $MAXTHREADDUR +1"
for s in $(seq 1 $RND) #simulate random lifespan
do
sleep 1;
(flock -x -w 10 201
trackthread $WORKERID $THREADID update $s
)201>/tmp/$ME.lock
done
(flock -x -w 10 201
trackthread $WORKERID $THREADID setfree
)201>/tmp/$ME.lock
(flock -x -w 10 201
updateIPC
)201>/tmp/$ME.lock
}
threadcount()
{
TC=$(ls /tmp/$ME.$F1* 2> /dev/null | wc -l)
#echo threadcount is $TC >> $DBG
THREADCOUNT=$TC
echo $TC
}
status()
{
#summary status line
COMPLETE=$(cat $IPC)
plot 1 $[$THREADLIMIT+2] "WORKERS $(threadcount)/$THREADLIMIT SPAWNED $SPAWNED/$SPAWN COMPLETE $COMPLETE/$SPAWN SF=$SPEEDFACTOR TIMING=$TPS"
echo -en '\033[K' #clear to end of line
}
function main()
{
while [ $SPAWNED -lt $SPAWN ]
do
while [ $(threadcount) -lt $THREADLIMIT ] && [ $SPAWNED -lt $SPAWN ]
do
WID=$(getfreeworkerid)
worker $WID $SPAWNED &
touch /tmp/$ME.$F1$WID #if this loops faster than file creation in the worker thread it steps on itself, thread tracking is best in main loop
SPAWNED=$[$SPAWNED+1]
(flock -x -w 10 201
status
)201>/tmp/$ME.lock
sleep $TPS
if ((! $[$SPAWNED%100]));then
#rethink thread timing every 100 threads
threadspeed
fi
done
sleep $TPS
done
while [ "$(threadcount)" -gt 0 ]
do
(flock -x -w 10 201
status
)201>/tmp/$ME.lock
sleep 1;
done
status
}
clear
threadspeed
main
wait
status
echo
Так как по какой-то причине я не могу использоватьwait
, я придумал это решение:
# create a hashmap of the tasks name -> its command
declare -A tasks=(
["Sleep 3 seconds"]="sleep 3"
["Check network"]="ping imdb.com"
["List dir"]="ls -la"
)
# execute each task in the background, redirecting their output to a custom file descriptor
fd=10
for task in "${!tasks[@]}"; do
script="${tasks[${task}]}"
eval "exec $fd< <(${script} 2>&1 || (echo $task failed with exit code \${?}! && touch tasks_failed))"
((fd+=1))
done
# print the outputs of the tasks and wait for them to finish
fd=10
for task in "${!tasks[@]}"; do
cat <&$fd
((fd+=1))
done
# determine the exit status
# by checking whether the file "tasks_failed" has been created
if [ -e tasks_failed ]; then
echo "Task(s) failed!"
exit 1
else
echo "All tasks finished without an error!"
exit 0
fi
Ваш сценарий должен выглядеть так:
prog1 &
prog2 &
.
.
progn &
wait
progn+1 &
progn+2 &
.
.
Предполагая, что ваша система может выполнять n заданий одновременно. используйте wait для одновременного выполнения только n заданий.
Здесь есть много интересных ответов, но я черпал вдохновение из этого ответа и собрал простой скрипт, который запускает несколько процессов параллельно и обрабатывает результаты после их завершения. Вы можете найти его в этом gist или ниже:
#!/usr/bin/env bash
# inspired by https://stackoverflow.com/a/29535256/2860309
pids=""
failures=0
function my_process() {
seconds_to_sleep=$1
exit_code=$2
sleep "$seconds_to_sleep"
return "$exit_code"
}
(my_process 1 0) &
pid=$!
pids+=" ${pid}"
echo "${pid}: 1 second to success"
(my_process 1 1) &
pid=$!
pids+=" ${pid}"
echo "${pid}: 1 second to failure"
(my_process 2 0) &
pid=$!
pids+=" ${pid}"
echo "${pid}: 2 seconds to success"
(my_process 2 1) &
pid=$!
pids+=" ${pid}"
echo "${pid}: 2 seconds to failure"
echo "..."
for pid in $pids; do
if wait "$pid"; then
echo "Process $pid succeeded"
else
echo "Process $pid failed"
failures=$((failures+1))
fi
done
echo
echo "${failures} failures detected"
Это приводит к:
86400: 1 second to success
86401: 1 second to failure
86402: 2 seconds to success
86404: 2 seconds to failure
...
Process 86400 succeeded
Process 86401 failed
Process 86402 succeeded
Process 86404 failed
2 failures detected
for ARG in $*; do
command $ARG &
NPROC=$(($NPROC+1))
if [ "$NPROC" -ge 4 ]; then
wait
NPROC=0
fi
done
Если у вас есть терминал с графическим интерфейсом, вы можете создать новый экземпляр терминала с вкладками для каждого процесса, который вы хотите запустить параллельно.
Это имеет то преимущество, что каждая программа запускается на своей собственной вкладке, где с ней можно взаимодействовать и управлять ею независимо от других запущенных программ.
Например, в Ubuntu 20.04:
gnome-terminal --tab -- bash -c 'prog1'
gnome-terminal --tab -- bash -c 'prog2'
Чтобы последовательно запускать определенные программы или другие команды, вы можете добавить;
gnome-terminal --tab -- bash -c 'prog1_1; prog1_2'
gnome-terminal --tab -- bash -c 'prog2'
Я обнаружил, что для некоторых программ терминал закрывается до их запуска. Для этих программ я добавляю команду терминала с; wait
или; sleep 1
gnome-terminal --tab -- bash -c 'prog1; wait'
Для Mac OS вам нужно будет найти эквивалентную команду для используемого вами терминала — я не тестировал Mac OS, так как у меня нет Mac.
Если ты:
- На Mac и есть iTerm
- Хотите запускать различные процессы, которые остаются открытыми долгое время / до Ctrl+C
- Хотите легко видеть результат каждого процесса
- Хотите иметь возможность легко останавливать определенный процесс с помощью Ctrl+C
Один из вариантов - написать сценарий для самого терминала, если ваш вариант использования связан с мониторингом / управлением приложениями.
Например, недавно я сделал следующее. Конечно, он специфичен для Mac, специфичен для iTerm и основан на устаревшем API-интерфейсе Apple Script (у iTerm есть более новый вариант Python). Он не получает награды за элегантность, но выполняет свою работу.
#!/bin/sh
root_path="~/root-path"
auth_api_script="$root_path/auth-path/auth-script.sh"
admin_api_proj="$root_path/admin-path/admin.csproj"
agent_proj="$root_path/agent-path/agent.csproj"
dashboard_path="$root_path/dashboard-web"
osascript <<THEEND
tell application "iTerm"
set newWindow to (create window with default profile)
tell current session of newWindow
set name to "Auth API"
write text "pushd $root_path && $auth_api_script"
end tell
tell newWindow
set newTab to (create tab with default profile)
tell current session of newTab
set name to "Admin API"
write text "dotnet run --debug -p $admin_api_proj"
end tell
end tell
tell newWindow
set newTab to (create tab with default profile)
tell current session of newTab
set name to "Agent"
write text "dotnet run --debug -p $agent_proj"
end tell
end tell
tell newWindow
set newTab to (create tab with default profile)
tell current session of newTab
set name to "Dashboard"
write text "pushd $dashboard_path; ng serve -o"
end tell
end tell
end tell
THEEND
С помощью bashj ( https://sourceforge.net/projects/bashj/) вы сможете запускать не только несколько процессов (как предлагали другие), но и несколько потоков в одной JVM, управляемой из вашего сценария. Но, конечно, это требует Java JDK. Потоки потребляют меньше ресурсов, чем процессы.
Вот рабочий код:
#!/usr/bin/bashj
#!java
public static int cnt=0;
private static void loop() {u.p("java says cnt= "+(cnt++));u.sleep(1.0);}
public static void startThread()
{(new Thread(() -> {while (true) {loop();}})).start();}
#!bashj
j.startThread()
while [ j.cnt -lt 4 ]
do
echo "bash views cnt=" j.cnt
sleep 0.5
done