Back to home

Bash中的并行

管道

通过标准输入输出,通过管道可以解决许多问题。

find /  |  gzip -c >tree.gz

一个程序的执行结果提供给下一个程序作为输入。

你还可以组合多条命令:

echo "My home dir:"; find ~ ; echo "My web site:"; find /var/www  | gzip -c >my-fs-tree.gz

线程池

xargs 指令有一个不常见的功能,提供线程池!(其实是进程池)

seq 15 | xargs -n 1 echo

seq 15 | xargs --max-procs=4 -n 1 echo

给定—max-procs 参数即可指定将任务拆分为指定数量的进程同时进行。比如:

seq 1000 | xargs -P 50 -I NONE lwp-request http://localhost:3000/

这条指令指定了50个进程处理1000个任务,-I NONE 参数指定了输入参数的占位符是NONE在后面的输入中可以用于替换xargs的输出。

# That's commands.txt file

echo Hello world

echo Goodbye world

echo Goodbye cruel world

对于这样的一个文本,我们通过执行如下语句去批次执行这些语句:

cat commands.txt | xargs -I CMD --max-procs=3 bash -c CMD

如果你不知道xargs的这个特性,那么你肯有可能写出这样的代码

xargs 中自动转义指令

直接将指令以字符串的形式传入xargs可能会出现转义问题:

echo 'echo -e "first\nsecond"' | xargs --max-procs=3 -I CMD bash -c CMD

# Output is:

# firstnsecond

看到没?多了一个”n”在两个单词之间。在Bash中,你不需要GNU parallel 去解决这种问题。只需要 printf%q格式化就能原样打印出转义后的指令。

printf "%q\n" 'echo -e "first\nsecond"' | xargs --max-procs=4 -I CMD bash -c CMD

比如:

# That's commands.txt file

cp "file 1" "file 101"

cp "file 2" "file 102"

cp "file 3" "file 103”
cat commands.txt | while read i; do printf "%q\n" "$i"; done | xargs --max-procs=3 -I CMD bash -c CMD

通过上面的语句就能完美并发执行。

有依赖的并发

make工具就是一个极好的例子。你一定知道make -j 5 可以指定五个工作进程同时进行,而且它还能自动计算出依赖。

Bash 解决依赖的办法有很多,其中flock命令就提供了一种基于锁的方法,这里给出了一些例子

子进程和进程替换

Bash 有一个方便的语法用于分配一个新的子进程。这个功能不像 xargs,在Bash语言中内置,并且使得交互更加紧凑。

每一个Shell的交互,都有他们自己的文件描述符以及环境变量。这些继承自调用shell,能够被重写,但是任何修改都不会影响父shell。

Bash 还有一个叫做进程替换的功能。他会分配一个子进程并且用它的输出输出代替父进程的一个命名管道(比如 “/dev/fd/63")。

假设对于一个输入,有两类文本需要不同的进程处理。这可以通过分配一个子进程拆分输入,然后分别启动两个处理进程通过不同的命名管道处理。这三个进程不仅是并行执行,而且只需要惊人的几行代码就能实现。

#!/bin/bash
line=unharmed

# Synchronization "flags"
sync1=`mktemp`
sync2=`mktemp`

( while read line ; do
    echo "$line" | grep 'aaa' 1>&100
    echo "$line" | grep 'bbb' 1>&101
  done;
) \
100> >( while read line ; do echo "$line" | sed -e 's/x/y/g'; sleep 1 ; done; rm $sync1 ) \
101> >( while read line ; do echo "$line" | sed -e 's/x/z/g'; sleep 2 ; done; rm $sync2 ) \
   < <( for ((i=0;i<6;i++)) ; do echo aaaxxx; echo bbbxxx ; done; echo ooops )

# "Synchronize" subprocesses
while [ -f "$sync1" ] ; do sleep 1 ; done
while [ -f "$sync2" ] ; do sleep 1 ; done

echo "The \$line is unharmed : $line"
# Since all above are subshells, there won't be any effect on $line variable:
#    The $line is unharmed : unharmed

输出结果:

pavel@lonely ~/social/blog/bash $ bash split.sh
aaayyy
bbbzzz
aaayyy
aaayyy
bbbzzz
aaayyy
bbbzzz
aaayyy
aaayyy
bbbzzz
bbbzzz
bbbzzz
The $line is unharmed : unharmed

如果我们不加文件锁,上面的子进程仍然会在父进程结束后继续执行。我不知道如何干净的处理子进程的同步,不过好消息是如果你仅仅只是需要输入,而不关心输出(比如归档压缩),那么这个方案已经满足你的要求。

通过 flock 互斥

我们现在看到的是类似于管道的同步,中心处理进程需要等待其他进程的管道数据处理完。这里有另外一种同步,进程等待直到共享锁释放,互斥。在Linux操作系统,你能够使用 flock 功能实现。

flock 方法以独占的方式打开一个文件描述符,如果还它已经被 flock 打开,就会一直等待。文件描述符关闭时,共享锁就释放。有点像“命名锁”,它实际上也是。

比如如何同时对同一目录进行操作:

( flock 201; make -C $dir ; ) 201>lock_installing_into_$dir

试试下面的代码:

#!/bin/bash

function inst ()
{ ( flock 201
echo "Installed $@"
sleep 2
) 201>installing_into_$@
}

inst a &
inst b &
inst a &
inst a &
inst a &
inst a &
inst c &
inst a &

wait

sleep执行结束之前,操作步骤a都被Block直到上一次的 flock 文件关闭:

pavel@lonely ~/social/blog/bash $ bash flock.sh
Installed b
Installed c
Installed a
Installed a
Installed a
Installed a
Installed a
Installed a

但是flock 不会阻塞锁文件的所有读写操作。他只阻塞其他 flocks。所以你必须以 flock 的方式打开文件。这只是互斥,不是读写保护。

通过其他工具

当我写到这里是,我发现 Bash 的帮助页面给出了 coproc 关键字,犹豫这个是 4.0 之后引入的,我不太熟悉。这里有一篇关于它很好的文章

结论

这些是我所知的 Bash 在 Linux 有效的同步原语。我喜欢它能帮助你写出更有威力的 shell 脚本。

原文