【问题标题】:Parallelize Bash script with maximum number of processes使用最大进程数并行化 Bash 脚本
【发布时间】:2008-09-01 16:47:53
【问题描述】:

假设我在 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

这将立即运行 all do-somethings,但有几个缺点,主要是 do-something 也可能有一些重要的 I/O 执行 all 一下子可能会慢一点。另一个问题是这个代码块是立即返回的,所以当所有的do-somethings都完成后,就没有办法做其他工作了。

您将如何编写此循环以使始终有 X 个do-somethings 同时运行?

【问题讨论】:

  • 作为一个sidenode,我梦想将make的 -j 选项添加到原始的bash中。它并不总是有效,但对于一些简单的情况,你知道循环的主体将为每次迭代做一些独特的事情,只说“for -j 4 ...”会很干净。
  • 交叉引用 stackoverflow.com/questions/1537956/… 以获得 bash 解决方案,该解决方案可缓解性能问题并允许将子进程组分开。
  • 我会推荐我的解决方案stackoverflow.com/a/28965927/340581

标签: bash


【解决方案1】:

根据您想要做什么,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.

【讨论】:

  • 在我看来,这种方法是最优雅的解决方案。除了,因为我偏执,我总是喜欢使用find [...] -print0 和xargs -0。
  • cpus=$(getconf _NPROCESSORS_ONLN)
  • 从手册上看,为什么不用--max-procs=0来获取尽可能多的进程呢?
  • @EverythingRightPlace,该问题明确要求的进程不超过可用处理器。 --max-procs=0 更像是提问者的尝试(启动与参数一样多的进程)。
  • 我想知道这个解决方案如何在问题的 for 循环中实现?
【解决方案2】:

使用 GNU Parallel 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

【讨论】:

  • 请注意,这在使用find 命令生成文件列表时非常有用,因为它不仅可以防止for i in ...; do 中出现文件名中有空格的问题,而且 find 可以还有find -name \*.extension1 -or -name \*.extension2 GNU parallel 的 {.} 可以很好地处理。
  • 加 1 虽然 cat 当然是 useless.
  • @tripleee 回复:无用的猫。见oletange.blogspot.dk/2013/10/useless-use-of-cat.html
  • 哦,是你!顺便说一句,你能更新那个博客上的链接吗?遗憾的是,partmaps.org 位置已死,但 Iki 重定向器应该继续工作。
  • @Hoov 我曾在 3 次看到文件中的换行符:抢救出现读取错误的磁盘、我自己的测试和犯罪分子。如果您只是处理文件,您自己或您信任的人制作,您应该没问题。但如果文件是从不受信任的来源上传的,则应使用-print0/-0。
【解决方案3】:
最大工作=4 并行化(){ 而 [ $# -gt 0 ] ;做 jobcnt=(`jobs -p`) if [ ${#jobcnt[@]} -lt $maxjobs ] ;然后 做某事 $1 & 转移 别的 睡觉 1 菲 完毕 等待 } 并行化 arg1 arg2 "5 args 到第三个工作" arg4 ...

【讨论】:

  • 意识到这里有一些 严重 引用不足,因此任何需要在参数中使用空格的作业都会严重失败;此外,如果请求的作业多于 maxjobs 允许的数量,则此脚本会在等待某些作业完成时占用您的 CPU。
  • 另请注意,这假设您的脚本没有对作业做任何其他事情;如果是,它也会将这些计入 maxjobs。
  • 您可能想使用“jobs -pr”来限制正在运行的作业。
  • 添加了一个 sleep 命令以防止 while 循环在没有任何中断的情况下重复,同时它等待已经运行的 do-something 命令完成。否则,这个循环基本上会占用一个 CPU 核心。这也解决了@lhunath 的担忧。
【解决方案4】:

这里有一个替代解决方案,可以插入到 .bashrc 并用于日常一个班轮:

function pwait() {
    while [ $(jobs -p | wc -l) -ge $1 ]; do
        sleep 1
    done
}

要使用它,只需将&放在jobs和pwait调用之后,参数给出并行进程数:

for i in *; do
    do_something $i &
    pwait 10
done

使用wait而不是忙于等待jobs -p的输出会更好,但似乎没有一个明显的解决方案来等待任何给定的作业完成而不是全部他们。

【讨论】:

  • 这个解决方案对我很有效。我真的很喜欢你实现它的方式。
【解决方案5】:

使用 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 之后,您只需检查变量包含的内容。

【讨论】:

  • 感谢您,即使代码尚未完成,它也为我提供了工作中遇到的问题的答案。
  • 唯一的麻烦是如果你杀死前台脚本(带有循环的那个)正在运行的作业不会被一起杀死
【解决方案6】:

也许尝试使用并行化实用程序而不是重写循环?我是 xjobs 的忠实粉丝。我一直使用 xjobs 在我们的网络上大量复制文件,通常是在设置新的数据库服务器时。 http://www.maier-komor.de/xjobs.html

【讨论】:

    【解决方案7】:

    如果您熟悉make 命令,大多数时候您可以将要运行的命令列表表示为生成文件。例如,如果您需要对文件 *.input 运行 $SOME_COMMAND,每个文件都会产生 *.output,您可以使用 makefile

    输入 = a.输入 b.输入 输出 = $(输入:.输入=.输出) %。输出输入 $(SOME_COMMAND) $

    然后运行

    使 -j

    最多并行运行 NUMBER 个命令。

    【讨论】:

      【解决方案8】:

      虽然在bash 中正确执行此操作可能是不可能的,但您可以相当轻松地执行半正确操作。 bstark 给出了一个公平的近似值,但他有以下缺陷:

      • 分词:您不能将任何作业传递给其参数中使用以下任何字符:空格、制表符、换行符、星号、问号。如果你这样做了,事情就会破裂,可能会出乎意料。
      • 它依赖于脚本的其余部分,而不是任何背景。如果你这样做了,或者稍后你在后台发送的脚本中添加了一些内容,因为你忘记了因为他的 sn-p 而不允许使用后台作业,那么事情就会中断。

      另一个没有这些缺陷的近似值如下:

      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 恰好是刚刚完成的作业,kill -0 不会注意到您的作业已经完成,事情会再次中断.

      bash 不可能有完美的解决方案。

      【讨论】:

        【解决方案9】:

        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
        

        【讨论】:

        • make -j 的使用很聪明,但没有任何解释和那一堆只写的 Awk 代码,我不赞成。
        【解决方案10】:

        我从事的项目使用 wait 命令来控制并行 shell(实际上是 ksh)进程。为了解决您对 IO 的担忧,在现代操作系统上,并行执行实际上可能会提高效率。如果所有进程都在读取磁盘上的相同块,则只有第一个进程必须访问物理硬件。其他进程通常能够从内存中操作系统的磁盘缓存中检索块。显然,从内存读取比从磁盘读取要快几个数量级。此外,该好处不需要更改编码。

        【讨论】:

          【解决方案11】:

          真的迟到了,但这里有另一个解决方案。

          许多解决方案不处理命令中的空格/特殊字符,不让 N 个作业始终运行,在繁忙的循环中消耗 cpu,或者依赖外部依赖项(例如 GNU parallel)。

          使用inspiration for dead/zombie process handling,这是一个纯 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
          

          【讨论】:

            【解决方案12】:

            这对于大多数目的来说可能已经足够了,但不是最佳的。

            #!/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
            

            【讨论】:

              【解决方案13】:

              以下是我在 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
              

              【讨论】:

                【解决方案14】:

                您可以使用简单的嵌套 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 数量。

                【讨论】:

                  【解决方案15】:

                  我的解决方案是始终保持给定数量的进程运行,跟踪错误并处理不可中断/僵尸进程:

                  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"
                  

                  【讨论】:

                    【解决方案16】:

                    $DOMAINS = "命令中某些域的列表" some-command 中的 foo 做

                    eval `some-command for $DOMAINS` &
                    
                        job[$i]=$!
                    
                        i=$(( i + 1))
                    

                    完成

                    Ndomains=echo $DOMAINS |wc -w

                    for i in $(seq 1 1 $Ndomains) 做 echo "等待 ${job[$i]}" 等待“${job[$i]}” 完成

                    在这个概念中将适用于并行化。重要的是 eval 的最后一行是 '&' 这会将命令放到背景中。

                    【讨论】:

                      猜你喜欢
                      • 2011-09-29
                      • 2017-04-12
                      • 1970-01-01
                      • 1970-01-01
                      • 2016-09-28
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多