按照shell语法,后一个前台命令必须等待前一个前台命令执行完毕才能进行,这就是所谓的单线程程序。如果两条命令之间有依赖性还好,否则后一条命令就白白浪费了等待的时间了。
网上查了一遍,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
运行测试:
# time sh simple.sh
我是0,运行了3秒,整个脚本执行了3秒
我是1,运行了3秒,整个脚本执行了6秒
我是2,运行了3秒,整个脚本执行了9秒
我是3,运行了3秒,整个脚本执行了12秒
我是4,运行了3秒,整个脚本执行了15秒
12
real 0m15.131s
user 0m0.046s
sys 0m0.079s
”我是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
运行测试:
# time sh multithreading.sh
我是2,运行了3秒,整个脚本执行了3秒
我是3,运行了3秒,整个脚本执行了3秒
我是1,运行了3秒,整个脚本执行了3秒
我是4,运行了3秒,整个脚本执行了3秒
我是0,运行了3秒,整个脚本执行了3秒
5
real 0m3.105s
user 0m0.065s
sys 0m0.115s
运行很快,而且很不老实(顺序都乱了,大概是因为expr运算所花时间不同)
解析:这一个脚本的变化是在命令后面增加了&标记,意思是将进程扔到后台。在shell中,后台命令之间是不区分先来后到关系的。所以各后台子进程会抢夺资源进行运算。
wait命令:
wait [n]
n 表示当前shell中某个执行的后台命令的pid,wait命令会等待该后台进程执行完毕才允许下一个shell语句执行;如果没指定则代表当前shell后台执行的语句,wait会等待到所有的后台程序执行完毕为止。
如果没有wait,后面的shell语句是不会等待后台进程的,一些对前面后台进程有依赖关系的命令执行就不正确了。例如wc命令就会提示aa不存在。
实例三:可控制后台进程数量的“多进程”shell
前一个shell也虽然达到“多进程”执行效果,但是并发执行的子进程数量却是不受控制的。
想想看,如果某一个作业需要执行1万次,那么到底是单进程实现还是像实例1那样多进程实现呢?前者,你慢慢等待吧;后者,祝你好运,希望你的计算机同时执行那么多进程不宕机,不卡死。
而实例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
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
执行测试
# sh multi.2.sh &
[1] 5360
sh multi.2.sh &
[1] 6418
[root@bogon shell]#我是2,运行了3秒,整个脚本执行了3秒
我是0,运行了3秒,整个脚本执行了3秒
我是1,运行了3秒,整个脚本执行了3秒
我是5,运行了3秒,整个脚本执行了6秒
我是3,运行了3秒,整个脚本执行了6秒
我是4,运行了3秒,整个脚本执行了6秒
我是6,运行了3秒,整个脚本执行了9秒
我是8,运行了3秒,整个脚本执行了9秒
我是7,运行了3秒,整个脚本执行了9秒
我是9,运行了3秒,整个脚本执行了12秒
10 aa
在另一个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
可以看到同一时间只有4个进程(其中一个是shell本身(pid=5360)),子进程数量得到了控制。
或者:
#!/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
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,实际编写时可以将该值作为一个参数传入。
相关
Related Posts
Linux Shell脚本入门教程系列之(二)第一个shell脚本
本文是Linux Shell脚本系列教程的第(二)篇,更多shell教程请看:Linux Shell脚本系列教程 通过上一篇教程的学习,相信大家已经能够对shell建立起一个大体的印象了,接下来,我们通过一个最简单的脚本来继续深入对shell的学习。 新建shell脚本 新建一个文件,扩展名为sh(sh代表shell),或者其他任意名字,其实扩展名并不影响脚本执行,见名知意就好,这里用sh是为了便于分辨。 在你新建的文件中输入以下内容: #!/bin/bash echo…
Linux Shell脚本入门教程系列之(一)Shell简介
本文是Linux Shell脚本系列教程的第(一)篇,更多shell教程请看:Linux Shell脚本系列教程 想要学习linux,shell知识必不可少,今天就给大家来简单介绍下shell的基本知识。 Shell简介 Shell自身是一个用C语言编写的程序,是用户来使用Unix或Linux的桥梁,用户的大部分工作都需要通过Shell来完成。只有熟练使用shell,才能熟练掌握linux。 可以说:Shell既是一种命令语言,又是一种程序设计语言。 作为命令语言,它可以交互式地解释和执行用户输入的命令;而作为程序设计语言,它可以定义各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。 Shell虽然不是Unix/Linux系统内核的一部分,但它调用了系统核心的大部分功能来执行程序、建立文件并以并行的方式来协调各个程序的运行。…
Linux: Shell脚本备份MySQL数据库, Linux shell script for database backup
Shell脚本是我们写不同类型命令的一种脚本,这些命令在这一个文件中就可以执行。我们也可以逐一敲入命令手动执行。如果我们要使用shell脚本就必须在一开始把这些命令写到一个文本文件中,以后就可以随意反复运行这些命令了。 我首先要在本文带给你的是完整脚本。后面会对该脚本做说明。我假定你已经知道shell scripting、 mysqldump和crontab。 适用操作系统:任何Linux或UNIX。 主脚本(用于备份mysql数据库): 该Shell脚本可以自动备份数据库。只要复制粘贴本脚本到文本编辑器中,输入数据库用户名、密码以及数据库名即可。我备份数据库使用的是mysqlump 命令。后面会对每行脚本命令进行说明。 1. 分别建立目录“backups”和“history” mkdir…