【问题标题】:Bash: limit the number of concurrent jobs? [duplicate]Bash:限制并发作业的数量? [复制]
【发布时间】:2010-12-05 00:13:03
【问题描述】:

有没有一种简单的方法可以限制 bash 中的并发作业数量?我的意思是当后台运行的并发作业超过 n 个时生成 & 阻塞。

我知道我可以用 ps | 来实现它grep 风格的技巧,但有没有更简单的方法?

【问题讨论】:

  • 我认为这个问题可能会对你有所帮助:stackoverflow.com/questions/38160/parallelize-bash-script
  • 所以,许多令人费解的答案,但没有办法告诉 bash “最多十个并发作业!”。估计那时就没有了。太糟糕了,这确实是一个不错的功能。

标签: bash shell concurrency


【解决方案1】:

如果您安装了 GNU Parallel http://www.gnu.org/software/parallel/,您可以这样做:

parallel gzip ::: *.log

每个 CPU 核心运行一个 gzip,直到所有日志文件都被 gzip 压缩。

如果它是更大循环的一部分,您可以改用sem

for i in *.log ; do
    echo $i Do more stuff here
    sem -j+0 gzip $i ";" echo done
done
sem --wait

它会做同样的事情,但让你有机会为每个文件做更多的事情。

如果没有为您的发行版打包 GNU Parallel,您可以通过以下方式安装 GNU Parallel:

$ (wget -O - pi.dk/3 || lynx -source pi.dk/3 || curl pi.dk/3/ || \
   fetch -o - http://pi.dk/3 ) > install.sh
$ sha1sum install.sh | grep 883c667e01eed62f975ad28b6d50e22a
12345678 883c667e 01eed62f 975ad28b 6d50e22a
$ md5sum install.sh | grep cc21b4c943fd03e93ae1ae49e28573c0
cc21b4c9 43fd03e9 3ae1ae49 e28573c0
$ sha512sum install.sh | grep da012ec113b49a54e705f86d51e784ebced224fdf
79945d9d 250b42a4 2067bb00 99da012e c113b49a 54e705f8 6d51e784 ebced224
fdff3f52 ca588d64 e75f6033 61bd543f d631f592 2f87ceb2 ab034149 6df84a35
$ bash install.sh

如果无法全局安装,它将下载,检查签名,并进行个人安装。

观看 GNU Parallel 的介绍视频以了解更多信息: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

【讨论】:

  • 这太棒了——并行命令也很棒,你甚至不需要循环。
  • ::: 语法已过时,尽管有一个选项可以启用它以实现向后兼容性,某些发行版默认启用(有点奇怪,因为手册中的示例将无法使用盒子)。
  • @tripleee ::: 自 2010722 以来一直受到支持,并且将在可预见的将来提供。但是,您的安装可能会在不告诉您的情况下尝试模仿 Tollef 的并行 - 这解释了为什么您觉得它很奇怪。删除 /etc/parallel/config 应该可以解决您的问题。
  • 输出中有一个非常烦人的引文简介。这甚至没有任何意义,除非是支持版权的……这对于 GNU 工具来说是很奇怪的。 → 您可以使用sem--will-cite 参数将其删除。他们在想什么?
  • 有什么理由不运行一次“--bibtex”或“--citation”(正如它所建议的那样)?
【解决方案2】:

一个小的 bash 脚本可以帮助你:

# content of script exec-async.sh
joblist=($(jobs -p))
while (( ${#joblist[*]} >= 3 ))
do
    sleep 1
    joblist=($(jobs -p))
done
$* &

如果你打电话:

. exec-async.sh sleep 10

...四次,前三个调用将立即返回,第四个调用将阻塞,直到运行的作业少于三个。

您需要在当前会话中启动此脚本,方法是在其前面加上 . 前缀,因为jobs 仅列出当前会话的作业。

里面的sleep很丑,但是我没找到办法等待第一个工作终止。

【讨论】:

  • 子进程会变成僵尸。应该在某个地方等待。
  • 十年后... Bash 的wait 命令现在有-n。如果您运行wait -n,它将等待next 作业(在此会话中)退出而不管PID。 :)
【解决方案3】:

以下脚本显示了一种使用函数执行此操作的方法。您可以将 bgxupdate()bgxlimit() 函数放在脚本中,或者将它们放在来自脚本的单独文件中:

. /path/to/bgx.sh

它的优点是您可以独立维护多组进程(例如,您可以运行一个限制为10 的组和另一个完全独立的组,限制为3)。

它使用 Bash 内置的 jobs 来获取子进程列表,但将它们保存在单独的变量中。在底部的循环中,您可以看到如何调用bgxlimit() 函数:

  1. 设置一个空组变量。
  2. 将其转至bgxgrp
  3. 调用bgxlimit() 并提供您要运行的限制和命令。
  4. 将新组转移回您的组变量。

当然,如果你只有一组,直接使用bgxgrp变量就可以了,不用传入传出。

#!/bin/bash

# bgxupdate - update active processes in a group.
#   Works by transferring each process to new group
#   if it is still active.
# in:  bgxgrp - current group of processes.
# out: bgxgrp - new group of processes.
# out: bgxcount - number of processes in new group.

bgxupdate() {
    bgxoldgrp=${bgxgrp}
    bgxgrp=""
    ((bgxcount = 0))
    bgxjobs=" $(jobs -pr | tr '\n' ' ')"
    for bgxpid in ${bgxoldgrp} ; do
        echo "${bgxjobs}" | grep " ${bgxpid} " >/dev/null 2>&1
        if [[ $? -eq 0 ]]; then
            bgxgrp="${bgxgrp} ${bgxpid}"
            ((bgxcount++))
        fi
    done
}

# bgxlimit - start a sub-process with a limit.

#   Loops, calling bgxupdate until there is a free
#   slot to run another sub-process. Then runs it
#   an updates the process group.
# in:  $1     - the limit on processes.
# in:  $2+    - the command to run for new process.
# in:  bgxgrp - the current group of processes.
# out: bgxgrp - new group of processes

bgxlimit() {
    bgxmax=$1; shift
    bgxupdate
    while [[ ${bgxcount} -ge ${bgxmax} ]]; do
        sleep 1
        bgxupdate
    done
    if [[ "$1" != "-" ]]; then
        $* &
        bgxgrp="${bgxgrp} $!"
    fi
}

# Test program, create group and run 6 sleeps with
#   limit of 3.

group1=""
echo 0 $(date | awk '{print $4}') '[' ${group1} ']'
echo
for i in 1 2 3 4 5 6; do
    bgxgrp=${group1}; bgxlimit 3 sleep ${i}0; group1=${bgxgrp}
    echo ${i} $(date | awk '{print $4}') '[' ${group1} ']'
done

# Wait until all others are finished.

echo
bgxgrp=${group1}; bgxupdate; group1=${bgxgrp}
while [[ ${bgxcount} -ne 0 ]]; do
    oldcount=${bgxcount}
    while [[ ${oldcount} -eq ${bgxcount} ]]; do
        sleep 1
        bgxgrp=${group1}; bgxupdate; group1=${bgxgrp}
    done
    echo 9 $(date | awk '{print $4}') '[' ${group1} ']'
done

这是一个示例运行,插入了空白行以清楚地描绘不同的时间点:

0 12:38:00 [ ]
1 12:38:00 [ 3368 ]
2 12:38:00 [ 3368 5880 ]
3 12:38:00 [ 3368 5880 2524 ]

4 12:38:10 [ 5880 2524 1560 ]

5 12:38:20 [ 2524 1560 5032 ]

6 12:38:30 [ 1560 5032 5212 ]

9 12:38:50 [ 5032 5212 ]

9 12:39:10 [ 5212 ]

9 12:39:30 [ ]
  • 整个过程从12:38:00(时间t = 0)开始,如您所见,前三个进程立即运行。
  • 每个进程休眠10n 秒,第四个进程直到第一个退出(时间t = 10)才开始。可以看到进程3368在添加1560之前已经从列表中消失了。
  • 同样,第五个进程5032 开始于5880(第二个)在时间t = 20 退出。
  • 最后,第六个进程52122524(第三个)在t = 30 退出时开始。
  • 然后循环开始,第四个进程在时间 t = 50 退出(从 10 开始,持续时间为 40)。
  • 第五次退出时间为t = 70(从20 开始,持续时间为50)。
  • 最后,第六个退出时间为t = 90(从30 开始,持续时间为60)。

或者,如果您更喜欢图形化的时间线形式:

Process:  1  2  3  4  5  6 
--------  -  -  -  -  -  -
12:38:00  ^  ^  ^            1/2/3 start together.
12:38:10  v  |  |  ^         4 starts when 1 done.
12:38:20     v  |  |  ^      5 starts when 2 done.
12:38:30        v  |  |  ^   6 starts when 3 done.
12:38:40           |  |  |
12:38:50           v  |  |   4 ends.
12:39:00              |  |
12:39:10              v  |   5 ends.
12:39:20                 |
12:39:30                 v   6 ends.

【讨论】:

    【解决方案4】:

    这是最短的方法:

    waitforjobs() {
        while test $(jobs -p | wc -w) -ge "$1"; do wait -n; done
    }
    

    在派生任何新工作之前调用此函数:

    waitforjobs 10
    run_another_job &
    

    要在机器上拥有与内核一样多的后台作业,请使用 $(nproc) 而不是像 10 这样的固定数字。

    【讨论】:

    • 很棒,但对于 bash 版本 >= 4
    • 这有一个竞争条件——如果一个工作在你到达“等待”之前完成,那么你可能处于一个可以运行另一个工作的位置,但必须等到'等待'找到另一份工作。
    • 如何在脚本运行时终止它?它完全忽略了Ctrl+C 中断。
    【解决方案5】:

    假设你想写这样的代码:

    for x in $(seq 1 100); do     # 100 things we want to put into the background.
        max_bg_procs 5            # Define the limit. See below.
        your_intensive_job &
    done
    

    max_bg_procs 应该放在你的.bashrc 中:

    function max_bg_procs {
        if [[ $# -eq 0 ]] ; then
                echo "Usage: max_bg_procs NUM_PROCS.  Will wait until the number of background (&)"
                echo "           bash processes (as determined by 'jobs -pr') falls below NUM_PROCS"
                return
        fi
        local max_number=$((0 + ${1:-0}))
        while true; do
                local current_number=$(jobs -pr | wc -l)
                if [[ $current_number -lt $max_number ]]; then
                        break
                fi
                sleep 1
        done
    }
    

    【讨论】:

    • 我发现我需要使用“jobs -pr”而不是简单的“jobs -p”,否则它永远不会完成最后一个工作,并且如果我设置了限制就不会继续超过第一个工作一次做一份工作。
    【解决方案6】:

    以下函数(从上面的 tangens 答案开发,可以复制到脚本中或从文件中获取源代码):

    job_limit () {
        # Test for single positive integer input
        if (( $# == 1 )) && [[ $1 =~ ^[1-9][0-9]*$ ]]
        then
    
            # Check number of running jobs
            joblist=($(jobs -rp))
            while (( ${#joblist[*]} >= $1 ))
            do
    
                # Wait for any job to finish
                command='wait '${joblist[0]}
                for job in ${joblist[@]:1}
                do
                    command+=' || wait '$job
                done
                eval $command
                joblist=($(jobs -rp))
            done
       fi
    }
    

    1) 只需要插入一行来限制现有循环

    while :
    do
        task &
        job_limit `nproc`
    done
    

    2) 等待现有后台任务完成而不是轮询,提高快速任务的效率

    【讨论】:

    • 如果开始了 10 个作业,那么 job_limit 会等待所有 10 个作业完成,然后再开始另外 10 个作业,对吗?
    • 不,作业是一一开始的。当运行的作业达到限制时,job_limit 函数会阻塞。没有批处理。
    【解决方案7】:

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

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

    【讨论】:

    • 它有什么不好的地方?
    • 您开始 10 个作业,然后等待所有 10 个作业完成,然后再开始另外 10 个作业。有时您只运行 1 个作业而不是 10 个作业。如果您将慢速作业和快速作业混合在一起,这并不好。
    【解决方案8】:

    如果您愿意在纯 bash 之外执行此操作,则应该研究作业排队系统。

    例如,GNU queuePBS。对于 PBS,您可能需要查看 Maui 进行配置。

    这两个系统都需要一些配置,但完全有可能允许同时运行特定数量的作业,只有在运行作业完成时才启动新排队的作业。通常,这些作业排队系统将用于超级计算集群,您希望为任何给定的批处理作业分配特定数量的内存或计算时间;但是,没有理由不考虑计算时间或内存限制,就不能在一台台式计算机上使用其中之一。

    【讨论】:

      【解决方案9】:

      没有wait -n很难做到(例如busybox中的shell不支持)。所以这是一个解决方法,它不是最佳的,因为它每秒调用 10 次“作业”和“wc”命令。例如,如果您不介意等待更长的时间让每个作业完成,您可以将调用减少到每秒 1 次。

      # $1 = maximum concurent jobs
      #
      limit_jobs()
      {
         while true; do
            if [ "$(jobs -p | wc -l)" -lt "$1" ]; then break; fi
            usleep 100000
         done
      }
      
      # and now start some tasks:
      
      task &
      limit_jobs 2
      task &
      limit_jobs 2
      task &
      limit_jobs 2
      task &
      limit_jobs 2
      wait
      

      【讨论】:

        【解决方案10】:

        在 Linux 上,我使用它来将 bash 作业限制为可用 CPU 的数量(可能通过设置 CPU_NUMBER 来覆盖)。

        [ "$CPU_NUMBER" ] || CPU_NUMBER="`nproc 2>/dev/null || echo 1`"
        
        while [ "$1" ]; do
            {
                do something
                with $1
                in parallel
        
                echo "[$# items left] $1 done"
            } &
        
            while true; do
                # load the PIDs of all child processes to the array
                joblist=(`jobs -p`)
                if [ ${#joblist[*]} -ge "$CPU_NUMBER" ]; then
                    # when the job limit is reached, wait for *single* job to finish
                    wait -n
                else
                    # stop checking when we're below the limit
                    break
                fi
            done
            # it's great we executed zero external commands to check!
        
            shift
        done
        
        # wait for all currently active child processes
        wait
        

        【讨论】:

          【解决方案11】:

          您是否考虑过启动十个长时间运行的侦听器进程并通过命名管道与它们通信?

          【讨论】:

            【解决方案12】:

            你可以使用 ulimit -u 见http://ss64.com/bash/ulimit.html

            【讨论】:

            • 唯一的问题是它会导致进程死亡而不是阻塞和等待,这是期望的行为。
            • 此解决方案危险且难以控制。由于我的 shell 脚本往往包含大量子 shell 扩展和管道,因此每行通常需要 4 个以上的进程。当您设置整个进程的 ulimit 时,它不仅限制了可以执行的作业数,还限制了执行脚本其余部分所必需的东西,从而导致事情以不可预知的方式阻塞/失败。
            • 但这是您对用户实施限制的唯一方法
            【解决方案13】:

            Bash 主要是逐行处理文件。 因此,您可以将输入文件输入文件限制为 N 行,然后简单的模式适用:

            mkdir tmp ; pushd tmp ; split -l 50 ../mainfile.txt
            for file in * ; do 
               while read a b c ; do curl -s http://$a/$b/$c <$file &
               done ; wait ; done
            popd ; rm -rf tmp;
            

            【讨论】:

            • 如果我没看错的话,这会运行 50 个批次,然后等到所有这些都完成后再开始另一个批次。理想情况下,我们应该找到一种方法让 50 个并发进程始终运行。 (GNU parallel 很容易做到这一点,xargs 有一些稳定的说服力。)
            【解决方案14】:

            Wait 命令,-n 选项,等待下一个作业终止。

            maxjobs=10
            # wait for the amount of processes less to $maxjobs
            jobIds=($(jobs -p))
            len=${#jobIds[@]}
            while [ $len -ge $maxjobs ]; do
                # Wait until one job is finished
                wait -n $jobIds
                jobIds=($(jobs -p))
                len=${#jobIds[@]}
            done
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2018-12-18
              • 1970-01-01
              • 1970-01-01
              • 2017-07-03
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多