具有最大进程数的并行Bash脚本


86

可以说我在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,但是有一些缺点,主要是做某事可能还会有一些重要的I / O,一次执行所有操作可能会降低速度。另一个问题是该代码块立即返回,因此当所有do-somethings完成时,无法进行其他工作。

您将如何编写此循环,以便总是同时do-something运行X ?


2
作为一个sidenode,我梦想着将make的-j选项添加到bash中作为原始对象。这并不总是可行的,但是对于一些简单的情况,您知道循环的主体将为每次迭代做一些独特的事情,只说“ for -j 4 ...”是很干净的。
放松

1
交叉引用stackoverflow.com/questions/1537956/…以获取bash解决方案,该解决方案可缓解性能问题并允许子进程组保持独立。
paxdiablo

1
我建议我使用解决方案stackoverflow.com/a/28965927/340581
Tuttle 2015年

Answers:


62

根据您要执行的操作,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.

9
我认为这种方法是最优雅的解决方案。除了,由于我很偏执,所以我总是喜欢使用find [...] -print0xargs -0
amphetamachine

7
cpus=$(getconf _NPROCESSORS_ONLN)
mr.spuratic

1
从手册中,为什么不使用它--max-procs=0来获得尽可能多的进程?
EverythingRightPlace'3

@EverythingRightPlace,该问题明确要求不超过可用处理器的进程数。 --max-procs=0更像发问者的尝试(启动与参数一样多的进程)。
Toby Speight

39

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


1
请注意,这在使用find命令生成文件列表时非常有用,因为它不仅可以防止文件名中出现空格的问题,for i in ...; do而且find还能做到find -name \*.extension1 -or -name \*.extension2GNU parallel的{。}可以很好地处理。
Leo Izen 2013年

cat当然,加1毫无用处。
三胞胎


哦,原来是你!顺便说一句,您可以更新该博客上的链接吗?令人遗憾的是,partmaps.org的位置已死,但Iki重定向器应继续工作。
三胞胎

22
maxjobs = 4
并行化(){
        而[$#-gt 0]; 做
                jobcnt =(`jobs -p`)
                如果[$ {#jobcnt [@]} -lt $ maxjobs]; 然后
                        做某事$ 1&
                        转移  
                其他
                        睡1
                科幻
        完成
        等待
}

将arg1 arg2“ 5个args并行化到第三作业” arg4 ...

10
意识到这里存在一些严重的报价不足,因此任何需要在参数中加空格的作业都将严重失败;此外,如果请求的作业数量超过了maxjobs的允许范围,此脚本将在等待某些作业完成时吞噬CPU。
2009年

1
还要注意,这是假设您的脚本没有对作业执行任何其他操作。如果您愿意,它也将这些计入maxjobs。
2009年

1
您可能要使用“ jobs -pr”来限制正在运行的作业。
amphetamachine

1
添加了sleep命令,以防止while循环在没有中断的情况下重复进行,而它等待已运行的do-something命令完成。否则,此循环实际上将占用CPU内核之一。这也解决了@lhunath的担忧。
euphoria83

12

在这里,可以将替代解决方案插入.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,但是似乎没有明显的解决方案可以等到给定的任何作业完成而不是全部完成。


11

可以使用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您只需检查变量包含的内容即可。


1
为此,即使代码还没有完成,它也给了我解决我工作中问题的答案。
gerikson

唯一的麻烦是,如果您杀死前台脚本(带有循环的脚本),则正在运行的作业将不会一起被杀死
Girardi


7

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

输入= a。输入b。输入
输出= $(输入:。输入=。输出)

%。输出输入
    $(SOME_COMMAND)$ <$ @

全部:$(OUTPUT)

然后运行

使-j <NUMBER>

最多可以并行运行NUMBER个命令。


6

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

没有完美的解决方案bash


3

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代码,我不赞成这样做。
三胞胎

2

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


1

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

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

1

这是我设法在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

1

这里的聚会真的很晚,但这是另一个解决方案。

许多解决方案不处理命令中的空格/特殊字符,不始终保持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

0

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


0

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

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"

-1

$ DOMAINS =为富“的命令一些域名列表” some-command DO

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

完成

Ndomains =echo $DOMAINS |wc -w

为$(seq 1 1 $ Ndomains)中的i做echo“ wait for $ {job [$ i]}”“等待” $ {job [$ i]}“完成

在这个概念中将为并行化工作。重要的是eval的最后一行是'&',它将命令放到后台。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.