Linux: shell实现多线程, Forking / Multi-Threaded Processes | Bash, shell 线程池

按照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<$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
执行测试
# 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<$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

Leave a Reply