题
假设我在 Bash 中有一个循环:
for foo in `some-command`
do
do-something $foo
done
do-something
受 CPU 限制,我有一个漂亮闪亮的 4 核处理器。我希望能够跑到 4 do-something
马上就到了。
天真的方法似乎是:
for foo in `some-command`
do
do-something $foo &
done
这将运行 全部 do-something
一次,但有一些缺点,主要是 do-something 也可能有一些重要的 I/O 执行 全部 立刻可能会慢一点。另一个问题是这个代码块立即返回,所以当所有的代码块都没有办法做其他工作时 do-something
已经完成了。
你会如何编写这个循环,以便总是有 X do-something
立即运行吗?
解决方案
根据您想要执行的操作, xargs 也可以提供帮助(此处:使用 pdf2ps 转换文档):
cpus=$( ls -d /sys/devices/system/cpu/cpu[[:digit:]]* | wc -w )
find . -name \*.pdf | xargs --max-args=1 --max-procs=$cpus pdf2ps
来自文档:
--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.
其他提示
使用 GNU 并行 http://www.gnu.org/software/parallel/ 你可以写:
some-command | parallel do-something
GNU Parallel 还支持在远程计算机上运行作业。这将在远程计算机上的每个 CPU 核心上运行一个 - 即使它们具有不同数量的核心:
some-command | parallel -S server1,server2 do-something
一个更高级的例子:这里我们列出了希望 my_script 运行的文件。文件具有扩展名(可能是 .jpeg)。我们希望 my_script 的输出放在 basename.out 中的文件旁边(例如foo.jpeg -> foo.out)。我们希望为计算机拥有的每个核心运行一次 my_script,并且我们也希望在本地计算机上运行它。对于远程计算机,我们希望将要处理的文件传输到给定计算机。当 my_script 完成时,我们希望 foo.out 传回,然后我们希望 foo.jpeg 和 foo.out 从远程计算机中删除:
cat list_of_files | \
parallel --trc {.}.out -S server1,server2,: \
"my_script {} > {.}.out"
GNU Parallel 确保每个作业的输出不会混合,因此您可以使用输出作为另一个程序的输入:
some-command | parallel do-something | postprocess
请观看视频了解更多示例: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1
maxjobs=4 parallelize () { while [ $# -gt 0 ] ; do jobcnt=(`jobs -p`) if [ ${#jobcnt[@]} -lt $maxjobs ] ; then do-something $1 & shift else sleep 1 fi done wait } parallelize arg1 arg2 "5 args to third job" arg4 ...
使用 Makefile 代替普通的 bash,然后使用以下命令指定同时作业的数量 make -jX
其中 X 是同时运行的作业数。
或者你可以使用 wait
("man wait
”):启动多个子进程,调用 wait
- 当子进程完成时它将退出。
maxjobs = 10
foreach line in `cat file.txt` {
jobsrunning = 0
while jobsrunning < maxjobs {
do job &
jobsrunning += 1
}
wait
}
job ( ){
...
}
如果您需要存储作业的结果,请将其结果分配给变量。后 wait
您只需检查变量包含的内容即可。
这是一种替代解决方案,可以插入 .bashrc 并用于日常单行:
function pwait() {
while [ $(jobs -p | wc -l) -ge $1 ]; do
sleep 1
done
}
要使用它,只需将 &
在作业和 pwait 调用之后,该参数给出并行进程的数量:
for i in *; do
do_something $i &
pwait 10
done
使用起来会更好 wait
而不是忙着等待输出 jobs -p
, ,但似乎没有一个明显的解决方案可以等到任何给定的作业完成而不是全部完成。
也许尝试并行化实用程序而不是重写循环?我是 xjobs 的忠实粉丝。我一直使用 xjobs 通过网络批量复制文件,通常是在设置新的数据库服务器时。http://www.maier-komor.de/xjobs.html
在正确执行此操作时 bash
可能是不可能的,你可以相当容易地做半右。 bstark
给出了正确的近似值,但他有以下缺陷:
- 分词:您不能将任何在参数中使用以下字符的作业传递给它:空格、制表符、换行符、星号、问号。如果你这样做,事情就会破裂,而且可能会出乎意料。
- 它依赖于脚本的其余部分而不是任何背景。如果您这样做,或者稍后您在后台发送的脚本中添加了一些内容,因为您忘记了由于他的代码片段而不允许您使用后台作业,那么事情就会崩溃。
另一种没有这些缺陷的近似值如下:
scheduleAll() {
local job i=0 max=4 pids=()
for job; do
(( ++i % max == 0 )) && {
wait "${pids[@]}"
pids=()
}
bash -c "$job" & pids+=("$!")
done
wait "${pids[@]}"
}
请注意,此任务很容易适应,还可以在每个作业结束时检查其退出代码,因此您可以在作业失败时警告用户或为作业设置退出代码 scheduleAll
根据失败的工作数量,或者其他什么。
这段代码的问题在于:
- 它一次安排四个(在本例中)作业,然后等待所有四个作业结束。有些可能会比其他作业完成得早,这将导致下一批四个作业要等待,直到上一批作业中最长的作业完成。
解决最后一个问题的解决方案必须使用 kill -0
轮询是否有任何进程消失而不是 wait
并安排下一个工作。然而,这引入了一个新的小问题:你在工作结束和 kill -0
检查是否结束。如果作业结束并且系统上的另一个进程同时启动,则采用随机 PID(恰好是刚刚完成的作业的 PID), kill -0
不会注意到你的工作已经完成,事情会再次崩溃。
完美的解决方案是不可能的 bash
.
如果您熟悉 make
命令,大多数时候您可以将要运行的命令列表表达为 makefile。例如,如果您需要在文件 *.input 上运行 $SOME_COMMAND,每个文件都会生成 *.output,则可以使用 makefile
INPUT = a.input b.input OUTPUT = $(INPUT:.input=.output) %.output : %.input $(SOME_COMMAND) $< $@ all: $(OUTPUT)
然后运行
make -j<NUMBER>
最多并行运行 NUMBER 个命令。
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
}
使用:
cat my_commands | parallel -j 4
我从事的项目使用 等待 控制并行 shell(实际上是 ksh)进程的命令。为了解决您对 IO 的担忧,在现代操作系统上,并行执行实际上可能会提高效率。如果所有进程都读取磁盘上的相同块,则只有第一个进程必须访问物理硬件。其他进程通常能够从内存中操作系统的磁盘缓存中检索该块。显然,从内存读取比从磁盘读取快几个数量级。此外,好处是无需更改代码。
对于大多数目的来说,这可能已经足够了,但并不是最佳的。
#!/bin/bash
n=0
maxjobs=10
for i in *.m4a ; do
# ( DO SOMETHING ) &
# limit jobs
if (( $(($((++n)) % $maxjobs)) == 0 )) ; then
wait # wait until all have finished (not optimal, but most times good enough)
echo $n wait
fi
done
真的 这里的聚会迟到了,但这里有另一个解决方案。
许多解决方案不处理命令中的空格/特殊字符,不保持 N 个作业始终运行,在繁忙的循环中消耗 cpu,或依赖外部依赖项(例如GNU parallel
).
和 死亡/僵尸进程处理的灵感, ,这是一个纯粹的 bash 解决方案:
function run_parallel_jobs {
local concurrent_max=$1
local callback=$2
local cmds=("${@:3}")
local jobs=( )
while [[ "${#cmds[@]}" -gt 0 ]] || [[ "${#jobs[@]}" -gt 0 ]]; do
while [[ "${#jobs[@]}" -lt $concurrent_max ]] && [[ "${#cmds[@]}" -gt 0 ]]; do
local cmd="${cmds[0]}"
cmds=("${cmds[@]:1}")
bash -c "$cmd" &
jobs+=($!)
done
local job="${jobs[0]}"
jobs=("${jobs[@]:1}")
local state="$(ps -p $job -o state= 2>/dev/null)"
if [[ "$state" == "D" ]] || [[ "$state" == "Z" ]]; then
$callback $job
else
wait $job
$callback $job $?
fi
done
}
以及示例用法:
function job_done {
if [[ $# -lt 2 ]]; then
echo "PID $1 died unexpectedly"
else
echo "PID $1 exited $2"
fi
}
cmds=( \
"echo 1; sleep 1; exit 1" \
"echo 2; sleep 2; exit 2" \
"echo 3; sleep 3; exit 3" \
"echo 4; sleep 4; exit 4" \
"echo 5; sleep 5; exit 5" \
)
# cpus="$(getconf _NPROCESSORS_ONLN)"
cpus=3
run_parallel_jobs $cpus "job_done" "${cmds[@]}"
输出:
1
2
3
PID 56712 exited 1
4
PID 56713 exited 2
5
PID 56714 exited 3
PID 56720 exited 4
PID 56724 exited 5
对于每个进程的输出处理 $$
可用于记录到文件,例如:
function job_done {
cat "$1.log"
}
cmds=( \
"echo 1 \$\$ >\$\$.log" \
"echo 2 \$\$ >\$\$.log" \
)
run_parallel_jobs 2 "job_done" "${cmds[@]}"
输出:
1 56871
2 56872
您可以使用简单的嵌套 for 循环(用适当的整数替换下面的 N 和 M):
for i in {1..N}; do
(for j in {1..M}; do do_something; done & );
done
这将在 M 轮中执行 do_something N*M 次,每轮并行执行 N 个作业。您可以使 N 等于您拥有的 CPU 数量。
以下是我在 bash 脚本中设法解决此问题的方法:
#! /bin/bash
MAX_JOBS=32
FILE_LIST=($(cat ${1}))
echo Length ${#FILE_LIST[@]}
for ((INDEX=0; INDEX < ${#FILE_LIST[@]}; INDEX=$((${INDEX}+${MAX_JOBS})) ));
do
JOBS_RUNNING=0
while ((JOBS_RUNNING < MAX_JOBS))
do
I=$((${INDEX}+${JOBS_RUNNING}))
FILE=${FILE_LIST[${I}]}
if [ "$FILE" != "" ];then
echo $JOBS_RUNNING $FILE
./M22Checker ${FILE} &
else
echo $JOBS_RUNNING NULL &
fi
JOBS_RUNNING=$((JOBS_RUNNING+1))
done
wait
done
我的解决方案是始终保持给定数量的进程运行,跟踪错误并处理不可中断/僵尸进程:
function log {
echo "$1"
}
# Take a list of commands to run, runs them sequentially with numberOfProcesses commands simultaneously runs
# Returns the number of non zero exit codes from commands
function ParallelExec {
local numberOfProcesses="${1}" # Number of simultaneous commands to run
local commandsArg="${2}" # Semi-colon separated list of commands
local pid
local runningPids=0
local counter=0
local commandsArray
local pidsArray
local newPidsArray
local retval
local retvalAll=0
local pidState
local commandsArrayPid
IFS=';' read -r -a commandsArray <<< "$commandsArg"
log "Runnning ${#commandsArray[@]} commands in $numberOfProcesses simultaneous processes."
while [ $counter -lt "${#commandsArray[@]}" ] || [ ${#pidsArray[@]} -gt 0 ]; do
while [ $counter -lt "${#commandsArray[@]}" ] && [ ${#pidsArray[@]} -lt $numberOfProcesses ]; do
log "Running command [${commandsArray[$counter]}]."
eval "${commandsArray[$counter]}" &
pid=$!
pidsArray+=($pid)
commandsArrayPid[$pid]="${commandsArray[$counter]}"
counter=$((counter+1))
done
newPidsArray=()
for pid in "${pidsArray[@]}"; do
# Handle uninterruptible sleep state or zombies by ommiting them from running process array (How to kill that is already dead ? :)
if kill -0 $pid > /dev/null 2>&1; then
pidState=$(ps -p$pid -o state= 2 > /dev/null)
if [ "$pidState" != "D" ] && [ "$pidState" != "Z" ]; then
newPidsArray+=($pid)
fi
else
# pid is dead, get it's exit code from wait command
wait $pid
retval=$?
if [ $retval -ne 0 ]; then
log "Command [${commandsArrayPid[$pid]}] failed with exit code [$retval]."
retvalAll=$((retvalAll+1))
fi
fi
done
pidsArray=("${newPidsArray[@]}")
# Add a trivial sleep time so bash won't eat all CPU
sleep .05
done
return $retvalAll
}
用法:
cmds="du -csh /var;du -csh /tmp;sleep 3;du -csh /root;sleep 10; du -csh /home"
# Execute 2 processes at a time
ParallelExec 2 "$cmds"
# Execute 4 processes at a time
ParallelExec 4 "$cmds"
$ domains =“命令中某些域列表”的foo some-command
做
eval `some-command for $DOMAINS` &
job[$i]=$!
i=$(( i + 1))
完毕
N 域=echo $DOMAINS |wc -w
对于我in $(seq 1 1 $ ndomains)do echo“等待$ {job [$ i]}“ wait” $ {job [$ i]}“完成
在这个概念中将致力于并行化。重要的是评估的最后一行是“&”,它将将命令置于背景。