96

假设我在 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-somethings,但有几个缺点,主要是 do-something 也可能有一些重要的 I/O 一次执行所有可能会减慢一点。另一个问题是这个代码块立即返回,所以当所有的do-somethings 都完成时,没有办法做其他工作。

你将如何编写这个循环,这样总是有 X do-somethings 同时运行?

4

16 回答 16

69

根据您想要做什么,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.
于 2009-05-19T07:50:04.573 回答
41

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

于 2010-06-10T01:37:03.403 回答
22
最大工作=4
并行化(){
        而 [ $# -gt 0 ] ; 做
                jobcnt=(`jobs -p`)
                if [ ${#jobcnt[@]} -lt $maxjobs ] ; 然后
                        做某事 $1 &
                        转移  
                别的
                        睡觉 1
                菲
        完毕
        等待
}

并行化 arg1 arg2 "5 args 到第三个工作" arg4 ...
于 2008-09-01T18:00:43.140 回答
17

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

于 2009-05-19T03:40:40.603 回答
11

代替普通的 bash,使用 Makefile,然后指定同时运行的作业数,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您检查变量包含的内容之后。

于 2008-09-01T16:50:18.397 回答
8

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

于 2008-09-01T16:55:04.763 回答
7

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

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

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

全部:$(输出)

然后就跑

使 -j<NUMBER>

最多并行运行 NUMBER 个命令。

于 2009-05-21T20:33:30.260 回答
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,kill -0则不会注意到您的作业已经完成并且事情会再次中断。

完美的解决方案是不可能的bash

于 2009-05-19T07:26:10.167 回答
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
于 2012-02-22T10:14:33.200 回答
2

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

于 2008-09-03T23:19:27.773 回答
2

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

许多解决方案不处理命令中的空格/特殊字符,不让 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
于 2019-02-01T02:39:07.080 回答
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
于 2011-07-21T09:05:13.303 回答
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
于 2015-10-13T16:52:51.877 回答
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 数量。

于 2011-11-19T19:49:16.377 回答
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"
于 2016-08-28T08:30:57.300 回答
-1

$DOMAINS = "list of some domain in commands" for foo in some-command do

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

完毕

N域=echo $DOMAINS |wc -w

for i in $(seq 1 1 $Ndomains) do echo "wait for ${job[$i]}" wait "${job[$i]}" done

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

于 2014-01-16T08:18:00.323 回答