怎样在shell中实现并行执行
明天,有个朋友问我,他的shell中要执行一个迭代200次的循环,由于每一次循环都须要消耗一定的时间,执行上去比较慢,问我可不可以改成并行执行,循环一次执行10个任务,循环20次来完成所有的任务。
哪些是并行?并行,是一种常见的任务执行过程模式,指可以同时执行两个或多个程序,与之相对的则是串行。还应当注意,并行不是并发,二者之间是有显著区别的,有些开发者常常搞混。并发是指服务系统支持两个或多个任务同时存在,同时存在并不意味着同时执行,由于服务系统单位时间上只执行一个任务,其它的任务以等待的方式存在。
下边就朋友的问题,介绍怎样在shell中解决并行控制的方式。
串行改为并行
首先,先来看一个串行的事例:
> for i in `seq 1 10`
do
sleep 1; echo $i
done
这是一个迭代次数为10的循环,每一个循环就会等待1秒,执行总时长约等于10秒。sleep1会阻塞循环,只有sleep1执行结果,就会步入下一循环,这是典型的串行模式。
shell提供了一种把命令递交到后台任务队列的机制,即使用命令&将命令控制权交到后台并立刻返回执行下个任务。
> for i in `seq 1 10`
do
sleep 1 &; echo $i
done
还是这个反例,多了个&符linux循环执行脚本,其作用是将命令sleep1递交到后台去执行,而for无须等待就可步入下一次循环。所以前面的for循环在1秒未到的时间内就执行完毕,之后系统会挨个执行sleep1并向终端报告命令执行结束。
并行-等待模式
里面将串行循环改为并行循环的反例,并没考虑这样的情况。
> for i in `seq 1 10`
do
sleep 1 &; echo $i
done

echo "all weakup"
这个反例要求在for循环中的所有命令(sleep1)都执行完以后,复印“allweakup”。假如依照这段脚本,发觉情况并不是这样的,由于for循环不会等待sleep命令执行结束后才结束,而是把命令递交给系统后自己就退出了,以致还没有1个sleep执行完毕之前,“allweakup”就早已复印了。
为了达到题目要求,须要在echo”allweakup”命令之前,加上wait命令,意为等待里面所有&作用过的后台任务执行结束后才继续往下。
> for i in `seq 1 10`
do
sleep 1 &; echo $i
done
wait
echo "all weakup"
并行度控制
下边列出的方案并不包括所有可能的实现方案。
方案1-控制一次性递交的后台任务数目
前面的示例中,for循环会将所有命令转为后台执行。其实linux循环执行脚本,假如每位命令须要比较大的开支,但是循环次数太多,这个方式并不可取。这么,要求for循环有部份执行或循环达到一定次数后就要wait,等待前一批次的所递交的任务执行完以后,再递交一定数目命令(再循环一定次数)后再继续wait。其实这些方案并不高贵,但起码不会造成一次性向系统递交过多后台任务。
瞧瞧下边的反例:
degree=4
for i in `seq 1 10`
do
sleep 1 & # 提交到后台的任务
echo $i
[ `expr $i % $degree` -eq 0 ] && wait
done
里面示例,设置了一个变量degree,拿来表示并行度,在整个循环中控制阻塞的关键就是于句子[“expr$i%$degree“-eq0]&&wait,即在第n次循环时,假若n正好对degree求模等于0时,这么循环先阻塞,等待上面n个后台任务执行完毕后再继续,以这种推。
方案2-借助队列来控制递交的任务数目
并行度控制,原理还不算复杂,但由于shell的原生数据结构支持较弱,使用shell来实现并行度控制就比较麻烦。
与c、java、python等语言实现并行度的原理基本一致,都是设置一个类似线程池或则工作池的链表,
方案3-借助命名管线来做任务队列
大致原理是创建一个FIFO命名管线来做为队列linux系统界面,先放进一定量的字符到这个管线做为讯号。之后在一个for循环中,每循环一次,从管线中读取一个字符讯号,递交一个后台任务,并往这个管线中追加一个字符讯号,保持管线中的字符讯号数目。
是不是很像当java中的线程池、golang中的chan。没有接触过命名管理、文件描述符等概念的,可能会比较无法理解下边示例中的部份细节。
_fifofile="$$.fifo"
mkfifo $_fifofile # 创建一个FIFO类型的文件
exec 6$_fifofile # 将文件描述符6写入 FIFO 管道, 这里6也可以是其它数字
rm $_fifofile # 删也可以,
degree=4 # 定义并行度
#根据并行度设置信号个数
#事实上是在fd6中放置了$degree个回车符
for ((i=0;i&6
for i in `seq 1 20` # 循环20次
do

# 从管道中读取(消费掉)一个字符信号
# 当FD6中没有回车符时,停止,实现并行度控制
read -u6
{
sleep 1 # 实际任务的命令
echo $i
echo >&6 # 当进程结束以后,再向管道追加一个信号,保持管道中的信号总数量
} &
done
wait # 等待所有任务结束
exec 6>&- # 关闭管道
方案1-控制一次性递交的后台任务数目是采用分批次递交的思路,
而方案2则是借助消费命名管线时read会出现等待的特点,由于是每执行完一个任务后就追加一个讯号,等同释放一个任务执行名额,总是保持任务并行执行数为degree。
由于采用队列模型,当执行完一个任务,马上都会有另一个任务被启动,在服务系统设计时,这些模式的任务控制是系统资源借助率最高的。
shell脚本中$,,,#,$?含意
$0 这个程式的执行名字
$n 这个程式的第n个参数值,n=1…9
$* 这个程式的所有参数,此选项参数可超过9个。
$# 这个程式的参数个数
$$ 这个程式的PID(脚本运行的当前进程ID号)
$! 执行上一个背景指令的PID(后台运行的最后一个进程的进程ID号)
$? 执行上一个指令的返回值 (显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误)
$- 显示shell使用的当前选项,与set命令功能相同
@ 跟 @ 跟@跟*类似,但是可以当作数组用
命名管线命名管线处理的思路:
就相当于此时有10个沸水卧室,配有10把锁匙,此时有100个人要打开水,这么前10个人抢到锁匙,就先进去打开水,前面的90个人就须要等上面一个人下来后还回锁匙,在拿着锁匙进去打开水,这样才能控制100个人打开水的任务,同时不会将系统的资源一次性耗损太多,降低压力,降低处理速率。
知识点:
1、命名管线的特点
假如管线内容为空,则阻塞
管线具有读一个少一个,存一个读一个的性质,放回来的可以重复取
可以实现队列控制
2、如果管线放一段内容没有人取,则会阻塞
解决上述问题linux教程下载,可以通过文件描述符
文件描述符具有管线的所有特点,同时还具有一个管线不具有的特点:无限存不阻塞,无限取不阻塞,无需关注管线内容
命名管线创建方法
# 1、创建命名管道
mkfifo /tmp/fl
#2、创建文件描述符100,并关联到管道文件
exec 100/tmp/fl

#3、调用文件描述符,向管道里存放内容,同时也表示将用完的管道内容在放回管道
echo >&100
#4、读取文件描述符关联管道中的内容
`read -u100``
#5、关闭文件描述符的读和写
exec 100<&-
exec 100>&-
方案4-使用xargs命令的控制参数
我是在写本文的时侯,才发觉原先xargs有一个控制并行的参数。
> seq 20 | xargs -I % -P4 sh -c 'echo %; sleep 1s'
关键在于xargs的-P参数,指一次性接收多少个参数,默认为1。使用-I%指定在命令中可以使用%符来表示接收到的参数
方案5-使用parallel命令行工具
在gnulinux的生态中,有一个专门拿来处理本文所述并行控制场景的工具,名子叫parallel。
官方链接
下边还是使用复印序列号的事例来演示倘若控制并行:
> seq 20 | parallel -j 4 "echo {}; sleep 1"
命令格式与xargs类似,使用-j来指定并行度;使用{}来表示参数。