本文是Linux Shell脚本系列教程的第(二)篇,更多shell教程请看:Linux Shell脚本系列教程 通过上一篇教程的学习,相信大家已经能够对shell建立起一个大体的印象了,接下来,我们通过一个最简单的脚本来继续深入对shell的学习。 新建shell脚本 新建一个文件,扩展名为sh(sh代表shell),或者其他任意名字,其实扩展名并不影响脚本执行,见名知意就好,这里用sh是为了便于分辨。 在你新建的文件中输入以下内容: #!/bin/bash echo…
Linux: shell实现多线程, Forking / Multi-Threaded Processes | Bash, shell 线程池
引用网上例子:
实例一:全前台进程:
#!/bin/bash #filename:simple.sh starttime=$(date +%s) for ((i=0;i<5;i++));do { sleep 3;echo 1>>aa && endtime=$(date +%s) && echo "我是$i,运行了3秒,整个脚本执行了$(expr $endtime - $starttime)秒" } done cat aa|wc -l rm aa
”我是X,运行了3秒“就规规矩矩的顺序输出了。
实例二:使用’&’+wait 实现“多进程”实现
#!/bin/bash #filename:multithreading.sh starttime=$(date +%s) for ((i=0;i<5;i++));do { sleep 3;echo 1>>aa && endtime=$(date +%s) && echo "我是$i,运行了3秒,整个脚本执行了$(expr $endtime - $starttime)秒" }& done wait cat aa|wc -l rm aa
如果没有wait,后面的shell语句是不会等待后台进程的,一些对前面后台进程有依赖关系的命令执行就不正确了。例如wc命令就会提示aa不存在。
实例三:可控制后台进程数量的“多进程”shell
而实例3则可以实现可控制进程数量的多线程。
#!/bin/sh function a_sub { sleep 3; echo 1>>aa && endtime=$(date +%s) && echo "我是$i,运行了3秒,整个脚本执行 了$(expr $endtime - $starttime)秒" && echo } starttime=$(date +%s) export starttime tmp_fifofile="/tmp/$.fifo" echo $tmp_fifofile mkfifo $tmp_fifofile exec 6<>$tmp_fifofile rm $tmp_fifofile thread=3 for ((i=0;i<$thread;i++)); do echo done >&6 for ((i=0;i<10;i++)) do read -u6 { a_sub || {echo "a_sub is failed"} echo >&6 } & done wait exec 6>&- wc -l aa rm -f aa exit 0
在另一个ssh终端观察进程的运行
watch -d 'ps aux | grep multi.2.sh | grep -v grep' Every 2.0s: ps aux | grep multi.2.sh | grep -v grep Fri May 11 04:18:51 2012 root 5360 0.1 0.9 106084 1272 pts/0 S 04:18 0:00 sh multi.2.sh root 5400 0.0 0.3 106084 548 pts/0 S 04:18 0:00 sh multi.2.sh root 5401 0.0 0.3 106084 548 pts/0 S 04:18 0:00 sh multi.2.sh root 5404 0.0 0.3 106084 548 pts/0 S 04:18 0:00 sh multi.2.sh
或者:
#!/bin/bash # 2006-7-12, by wwy #———————————————————————————– # 此例子说明了一种用wait、read命令模拟多线程的一种技巧 # 此技巧往往用于多主机检查,比如ssh登录、ping等等这种单进程比较慢而不耗费cpu的情况 # 还说明了多线程的控制 #———————————————————————————– function a_sub { # 此处定义一个函数,作为一个线程(子进程) sleep 3 # 线程的作用是sleep 3s } tmp_fifofile="/tmp/$$.fifo" mkfifo $tmp_fifofile # 新建一个fifo类型的文件 exec 6<>$tmp_fifofile # 将fd6指向fifo类型 rm $tmp_fifofile thread=15 # 此处定义线程数 for ((i=0;i<$thread;i++));do echo done >&6 # 事实上就是在fd6中放置了$thread个回车符 for ((i=0;i<50;i++));do # 50次循环,可以理解为50个主机,或其他 read -u6 # 一个read -u6命令执行一次,就从fd6中减去一个回车符,然后向下执行, # fd6中没有回车符的时候,就停在这了,从而实现了线程数量控制 { # 此处子进程开始执行,被放到后台 a_sub && { # 此处可以用来判断子进程的逻辑 echo "a_sub is finished" } || { echo "sub error" } echo >&6 # 当进程结束以后,再向fd6中加上一个回车符,即补上了read -u6减去的那个 } & done wait # 等待所有的后台子进程结束 exec 6>&- # 关闭df6 exit 0
sleep 3s,线程数为15,一共循环50次,所以,此脚本一共的执行时间大约为12秒
即:
15×3=45, 所以 3 x 3s = 9s
(50-45=5)<15, 所以 1 x 3s = 3s
所以 9s + 3s = 12s
$ time ./multithread.sh >/dev/null real 0m12.025s user 0m0.020s sys 0m0.064s
而当不使用多线程技巧的时候,执行时间为:50 x 3s = 150s。
此程序中的命令
mkfifo tmpfile
和linux中的命令
mknod tmpfile p
效果相同。区别是mkfifo为POSIX标准,因此推荐使用它。该命令创建了一个先入先出的管道文件,并为其分配文件标志符6。管道文件是进程之 间通信的一种方式,注意这一句很重要
exec 6<>$tmp_fifofile # 将fd6指向fifo类型
如果没有这句,在向文件$tmp_fifofile或者&6写入数据时,程序会被阻塞,直到有read读出了管道文件中的数据为止。而执行了上面这一句后就可以在程序运行期间不断向fifo类 型的文件写入数据而不会阻塞,并且数据会被保存下来以供read程序读出。
总结一下:
方案1:使用”&”使命令后台运行
在linux中,在命令的末尾加上&
符号,则表示该命令将在后台执行,这样后面的命令不用等待前面的命令执行完就可以开始执行了。示例中的循环体内有多条命令,则可以以{}
括起来,在大括号后面添加&
符号。
#/bin/bash all_num=10 a=$(date +%H%M%S) for num in `seq 1 ${all_num}` do { sleep 1 echo ${num} } & done b=$(date +%H%M%S) echo -e "startTime:\t$a" echo -e "endTime:\t$b"
运行结果:
startTime: 194147 endTime: 194147 [j-tester@merger142 ~/bin/multiple_process]$ 1 2 3 4 5 6 7 8 9 10
[j-tester@merger142 ~/bin/multiple_process]$
(出现命令提示符表示脚本已运行完毕),然后才是数字的输出。这是因为循环体内的命令全部进入后台,所以均在sleep了1秒以后输出了数字。开始和结束时间相同,即循环体的执行时间不到1秒钟,这是由于循环体在后台执行,没有占用脚本主进程的时间。
方案2:命令后台运行+wait
命令
解决上面的问题,只需要在上述循环体的done语句后面加上wait
命令,该命令等待当前脚本进程下的子进程结束,再运行后面的语句。
#/bin/bash all_num=10 a=$(date +%H%M%S) for num in `seq 1 ${all_num}` do { sleep 1 echo ${num} } & done wait b=$(date +%H%M%S) echo -e "startTime:\t$a" echo -e "endTime:\t$b"
运行结果:
1 2 3 4 5 6 7 9 8 10 startTime: 194221 endTime: 194222
但这样依然存在一个问题:
因为&
使得所有循环体内的命令全部进入后台运行,那么倘若循环的次数很多,会使操作系统在瞬间创建出所有的子进程,这会非常消耗系统的资源。如果循环体内的命令又很消耗系统资源,则结果可想而知。
方案3:使用文件描述符控制并发数
最好的方法是并发的进程是可配置的。
#/bin/bash all_num=10 thread_num=5 a=$(date +%H%M%S) tempfifo="my_temp_fifo" mkfifo ${tempfifo} exec 6<>${tempfifo} rm -f ${tempfifo} for ((i=1;i<=${thread_num};i++)) do { echo } done >&6 for num in `seq 1 ${all_num}` do { read -u6 { sleep 1 echo ${num} echo "" >&6 } & } done wait exec 6>&- b=$(date +%H%M%S) echo -e "startTime:\t$a" echo -e "endTime:\t$b"
运行结果:
1 3 2 4 5 6 7 8 9 10 startTime: 195227 endTime: 195229
这个方案有时通不过 mkfifo ${tempfifo} ,那么就才用更简单的方法来控制:
for ARG in $*; do command $ARG & NPROC=$(($NPROC+1)) if [ "$NPROC" -ge 4 ]; then wait NPROC=0 fi done
4 是最大线程,command 是你要执行的代码。
方案4:使用xargs -P
控制并发数
xargs命令有一个-P
参数,表示支持的最大进程数,默认为1。为0时表示尽可能地大,即方案2
的效果。
#/bin/bash all_num=10 thread_num=5 a=$(date +%H%M%S) seq 1 ${all_num} | xargs -n 1 -I {} -P ${thread_num} sh -c "sleep 1;echo {}" b=$(date +%H%M%S) echo -e "startTime:\t$a" echo -e "endTime:\t$b"
运行结果:
1 2 3 4 5 6 8 7 9 10 startTime: 195257 endTime: 195259
方案5:使用GNU parallel
命令控制并发数
GNU parallel
命令是非常强大的并行计算命令,使用-j
参数控制其并发数量。
#/bin/bash all_num=10 thread_num=6 a=$(date +%H%M%S) parallel -j 5 "sleep 1;echo {}" ::: `seq 1 10` b=$(date +%H%M%S) echo -e "startTime:\t$a" echo -e "endTime:\t$b"
运行结果:
1 2 3 4 5 6 7 8 9 10 startTime: 195616 endTime: 195618
总结
“多线程”的好处不言而喻,虽然shell中并没有真正的多线程,但上述解决方案可以实现“多线程”的效果,重要的是,在实际编写脚本时应有这样的考虑和实现。
另外:
方案3、4、5虽然都可以控制并发数量,但方案3显然写起来太繁琐。
方案4和5都以非常简洁的形式完成了控制并发数的效果,但由于方案5的parallel命令非常强大,所以十分建议系统学习下。
方案3、4、5设置的并发数均为5,实际编写时可以将该值作为一个参数传入。
本文:Linux: shell实现多线程, Forking / Multi-Threaded Processes | Bash