将stdin传播到并行进程


13

我有一个处理标准输入上的文件列表的任务。程序的启动时间很长,每个文件花费的时间差异很大。我想产生大量的这些进程,然后将工作分派给不忙的人。有几种不同的命令行工具几乎可以满足我的要求,我将其缩小为两个几乎可以使用的选项:

find . -type f | split -n r/24 -u --filter="myjob"
find . -type f | parallel --pipe -u -l 1 myjob

问题是split进行纯循环,因此其中一个进程落后于并停留在后面,从而延迟了整个操作的完成;而parallel想要每N行或每字节输入生成一个进程,而我花了太多时间在启动开销上。

是否有这样的东西可以重用进程和馈送线到任何具有无阻塞标准输入的进程?


split命令从哪里来?该名称与标准文本处理实用程序冲突。
吉尔斯(Gilles)'所以

@Gilles,它是GNU之一:“拆分(GNU coreutils)8.13”。将其用作xargs的怪异替代品可能不是预期的用途,但它与我想要的最接近。
BCoates

2
我一直在考虑,一个基本的问题是知道的一个实例myjob准备接收更多输入。没有办法知道程序已准备好处理更多输入,您只能知道某个地方的某些缓冲区(管道缓冲区,stdio缓冲区)已准备好接收更多输入。准备就绪后,您可以安排程序发送某种请求(例如显示提示)吗?
吉尔斯(Gilles)'所以

假设该程序未在stdin上使用缓冲,则可以对read调用做出反应的FUSE文件系统就可以解决问题。那是相当大的编程努力。
吉尔(Gilles)'所以

为什么要-l 1parallelargs中使用?IIRC,告诉每个进程并行处理一行输入(即,每个myjob分支使用一个文件名,因此启动开销很大)。
cas 2012年

Answers:


1

在这种一般情况下,这似乎是不可能的。这意味着您为每个进程都有一个缓冲区,并且您可以从外部观察缓冲区来决定将下一个条目放在哪里(计划)...当然,您可能会编写一些东西(或使用类似slurm的批处理系统)

但是根据处理的过程,您可能可以对输入进行预处理。例如,如果您要下载文件,更新数据库中的条目或类似条目,但是其中的50%最终将被跳过(因此,根据输入,您的处理差异很大),则只需设置预处理器即可验证哪些条目将花费很长时间(文件存在,数据已更改,等等),因此,保证来自另一端的所有内容都将花费相当相等的时间。即使启发式方法不是完美的,您也可能会得到很大的改进。您可以将其他文件转储到文件中,然后以相同的方式进行处理。

但这取决于您的用例。


1

不,没有通用的解决方案。您的调度员需要知道每个程序何时准备读取另一行,而且我不知道有哪个标准允许这样做。您所能做的就是在STDOUT上放一行,等待消耗掉它。生产者在流水线上告诉下一个消费者是否准备就绪的方法并不是一个好方法。


0

我不这么认为。在我最喜欢的杂志上,曾经有一篇关于bash编程的文章,它做了您想要的。我愿意相信,如果有能够做到的工具,他们会提到它们。因此,您需要以下方面的东西:

set -m # enable job control
max_processes=8
concurrent_processes=0

child_has_ended() { concurrent_processes=$((concurrent_processes - 1)) }

trap child_has_ended SIGCHLD # that's magic calling our bash function when a child processes ends

for i in $(find . -type f)
do
  # don't do anything while there are max_processes running
  while [ ${concurrent_processes} -ge ${max_processes}]; do sleep 0.5; done 
  # increase the counter
  concurrent_processes=$((concurrent_processes + 1))
  # start a child process to actually deal with one file
  /path/to/script/to/handle/one/file $i &
done

显然,您可以根据自己的喜好更改对实际工作脚本的调用。我最初提到的杂志起着建立管道和实际启动工作线程之类的作用。mkfifo对此进行检查,但是由于工作进程需要向主进程发送信号,告知他们准备接收更多数据,因此该路由要复杂得多。因此,您需要为每个工作进程提供一个fifo来发送数据,而为主进程需要一个fifo以从工作人员那里接收数据。

免责声明 我从头开始写了那个脚本。它可能有一些语法问题。


1
这似乎不符合要求:您正在为每个项目启动不同的程序实例。
吉尔(Gilles)'所以

通常,最好使用find . -type f | while read i而不是for i in $(find . -type f)

0

对于GNU Parallel,可以使用--block设置块大小。但是,它确实需要您有足够的内存才能为每个正在运行的进程在内存中保留1个块。

我了解这并不完全是您要寻找的东西,但现在可能是可以接受的解决方法。

如果您的任务平均花费相同的时间,那么您也许可以使用mbuffer:

find . -type f | split -n r/24 -u --filter="mbuffer -m 2G | myjob"

0

尝试这个:

mkfifo 对于每个过程。

然后挂tail -f | myjob在每个fifo上。

例如设置工人(myjob流程)

mkdir /tmp/jobs
for X in 1 2 3 4
do
   mkfifo pipe$X
   tail -f pipe$X | myjob &
   jobs -l| awk '/pipe'$X'/ {print $2, "'pipe$X'"}' >> pipe-job-mapping
done

根据您的应用程序(myjob),您也许可以使用jobs -s查找停止的作业。否则,列出按CPU排序的进程,然后选择消耗最少资源的进程。具有工作报告本身,例如,当需要更多工作时,通过在文件系统中设置标志。

假设作业在等待输入时停止,请使用

jobs -sl 例如,找出停止的工作的pid并将其分配给工作

grep "^$STOPPED_PID" pipe-to-job-mapping | while read PID PIPE
do
   cat workset > $PIPE
done

我用

garfield:~$ cd /tmp
garfield:/tmp$ mkfifo f1
garfield:/tmp$ mkfifo f2
garfield:/tmp$ tail -f f1 | sed 's/^/1 /' &
[1] 21056
garfield:/tmp$ tail -f f2 | sed 's/^/2 /' &
[2] 21058
garfield:/tmp$ echo hello > f1
1 hello
garfield:/tmp$ echo what > f2
2 what
garfield:/tmp$ echo yes > f1
1 yes

我必须承认,这只是炮制的ymmv。


0

解决此问题真正需要的是某种类型的队列机制。

是否可以让作业从队列(例如SYSV消息队列)中读取其输入,然后让程序并行运行,只需将值推入队列中?

另一种可能性是对队列使用目录,如下所示:

  1. 查找输出会创建指向每个文件的符号链接,以在目录中进行处理, pending
  2. 每个作业过程中执行mv它看到在该目录下的兄弟目录的第一个文件pending,命名inprogress
  3. 如果作业成功移动了文件,则执行处理;否则,它将返回以从中查找并移动另一个文件名pending

0

详细阐述@ash的答案,您可以使用SYSV消息队列来分发工作。如果您不想用C编写自己的程序,则有一个实用程序ipcmd可以提供帮助。这是我将输出传递find $DIRECTORY -type f$PARALLEL多个进程的汇总:

set -o errexit
set -o nounset

export IPCMD_MSQID=$(ipcmd msgget)

DIRECTORY=$1
PARALLEL=$2

# clean up message queue on exit
trap 'ipcrm -q $IPCMD_MSQID' EXIT

for i in $(seq $PARALLEL); do
   {
      while true
      do
          message=$(ipcmd msgrcv) || exit
          [ -f $message ] || break
          sleep $((RANDOM/3000))
      done
   } &
done

find "$DIRECTORY" -type f | xargs ipcmd msgsnd

for i in $(seq $PARALLEL); do
   ipcmd msgsnd "/dev/null/bar"
done
wait

这是一个测试运行:

$ for i in $(seq 20 10 100) ; do time parallel.sh /usr/lib/ $i ; done
parallel.sh /usr/lib/ $i  0.30s user 0.67s system 0% cpu 1:57.23 total
parallel.sh /usr/lib/ $i  0.28s user 0.69s system 1% cpu 1:09.58 total
parallel.sh /usr/lib/ $i  0.19s user 0.80s system 1% cpu 1:05.29 total
parallel.sh /usr/lib/ $i  0.29s user 0.73s system 2% cpu 44.417 total
parallel.sh /usr/lib/ $i  0.25s user 0.80s system 2% cpu 37.353 total
parallel.sh /usr/lib/ $i  0.21s user 0.85s system 3% cpu 32.354 total
parallel.sh /usr/lib/ $i  0.30s user 0.82s system 3% cpu 28.542 total
parallel.sh /usr/lib/ $i  0.27s user 0.88s system 3% cpu 30.219 total
parallel.sh /usr/lib/ $i  0.34s user 0.84s system 4% cpu 26.535 total

0

除非您可以估计特定输入文件将被处理多长时间并且工作进程无法向调度程序报告(如它们在正常的并行计算方案中所做的操作-通常通过MPI进行),否则您通常会走运-支付一些处理输入的工人比其他工人更长的处罚(由于输入的不平等),或者支付为每个输入文件生成一个新进程的处罚。


0

在过去的7年中,GNU Parallel发生了变化。所以今天它可以做到:

此示例显示,与进程4和5相比,为进程11和10分配了更多的块,因为4和5读取速度较慢:

seq 1000000 |
  parallel -j8 --tag --roundrobin --pipe --block 1k 'pv -qL {}0000 | wc' ::: 11 4 5 6 9 8 7 10
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.