如何实现shell并发 



    很多人都问我如何写shell脚本,如何实现同时给三台ftp服务器上传文件,如何同时检测三台服务器是否alive等,其实这就是想实现shell的并发。那么shell并发该如何实现呢?

    下面我就拿这个例子来讲:


 


    每次任务都是输出字符“bingfa”,并停留一秒钟,共20次。


    按照正常思维,脚本应该这样写:


  1. [root@station1 ~]# cat a.sh
  2. #!/bin/bash
  3. for((i=0;i<20;i++))
  4. do
  5. sleep 1
  6. echo "bingfa"
  7. done
  8. [root@station1 ~]# time bash a.sh
  9. bingfa
  10. bingfa
  11. bingfa
  12. bingfa
  13. bingfa
  14. bingfa
  15. bingfa
  16. bingfa
  17. bingfa
  18. bingfa
  19. bingfa
  20. bingfa
  21. bingfa
  22. bingfa
  23. bingfa
  24. bingfa
  25. bingfa
  26. bingfa
  27. bingfa
  28. bingfa

  29. real 0m20.067s
  30. user 0m0.016s
  31. sys 0m0.031s
  32. [root@station1 ~]#

可以看到执行此脚本大概用了20秒。那么使用shell并发该怎么写,很多人都会想到后台程序,类似如下:

  1. [root@station1 ~]# cat b.sh
  2. #!/bin/bash
  3. for((i=0;i<20;i++))
  4. do
  5. {
  6. sleep 1
  7. echo "bingfa"
  8. }&
  9. done
  10. wait
  11. [root@station1 ~]# time bash b.sh
  12. bingfa
  13. bingfa
  14. bingfa
  15. bingfa
  16. bingfa
  17. bingfa
  18. bingfa
  19. bingfa
  20. bingfa
  21. bingfa
  22. bingfa
  23. bingfa
  24. bingfa
  25. bingfa
  26. bingfa
  27. bingfa
  28. bingfa
  29. bingfa
  30. bingfa
  31. bingfa

  32. real 0m1.060s
  33. user 0m0.005s
  34. sys 0m0.057s
  35. [root@station1 ~]#


这样写只需花大概一秒钟,可以看到所有的任务几乎同时执行,如果任务量非常大,系统肯定承受不了,也会影响系统中其他程序的运行,这样就需要一个线程数量的控制。下面是我一开始写的代码(是有问题的):


 


  1. [root@station1 ~]# cat c.sh
  2. #!/bin/bash
  3. exec 6<>tmpfile
  4. echo "1\n1\n1" &>6
  5. for((i=0;i<20;i++))
  6. do
  7. read -u 6
  8. {
  9. sleep 1
  10. echo "$REPLY"
  11. echo "1" 1>&6
  12. }&
  13. done
  14. wait
  15. [root@station1 ~]# time bash c.sh
  16. 111
  17. 1
  18. 1
  19. 1
  20. 1
  21. 1
  22. 1
  23. 1
  24. 1
  25. 1
  26. 1
  27. 1
  28. 1
  29. 1
  30. 1
  31. 1
  32. 1
  33. 1
  34. 1
  35. 1

  36. real 0m1.074s
  37. user 0m0.012s
  38. sys 0m0.031s
  39. [root@station1 ~]#


可以明显看出是有问题的,我本想控制线程个数为3,但是就算文件描述符6中为空,也会被读取空,然后跳过继续下面的执行,所以使用文件描述符打开一个文件是不行的,然后我就想着使用类似管道的文件来做,下面是我的代码:


 


  1. [root@station1 ~]# cat d.sh
  2. #!/bin/bash
  3. mkfifo fd2
  4. exec 9<>fd2
  5. echo -n -e "1\n1\n1\n" 1>&9

  6. for((i=0;i<20;i++))
  7. do
  8. read -u 9
  9. { #your process

  10. sleep 1
  11. echo "$REPLY"
  12. echo -ne "1\n" 1>&9
  13. } &
  14. done
  15. wait
  16. rm -f fd2
  17. [root@station1 ~]# time bash d.sh
  18. 1
  19. 1
  20. 1
  21. 1
  22. 1
  23. 1
  24. 1
  25. 1
  26. 1
  27. 1
  28. 1
  29. 1
  30. 1
  31. 1
  32. 1
  33. 1
  34. 1
  35. 1
  36. 1
  37. 1

  38. real 0m7.075s
  39. user 0m0.018s
  40. sys 0m0.044s
  41. [root@station1 ~]#

这样就ok了,三个线程运行20个任务,7秒多点。

 


 


 



shell如何实现多线程?

情景

shell脚本的执行效率虽高,但当任务量巨大时仍然需要较长的时间,尤其是需要执行一大批的命令时。因为默认情况下,shell脚本中的命令是串行执行的。如果这些命令相互之间是独立的,则可以使用“并发”的方式执行这些命令,这样可以更好地利用系统资源,提升运行效率,缩短脚本执行的时间。如果命令相互之间存在交互,则情况就复杂了,那么不建议使用shell脚本来完成多线程的实现。

为了方便阐述,使用一段测试代码。在这段代码中,通过​​seq​​命令输出1到10,使用​​for...in​​语句产生一个执行10次的循环。每一次循环都执行​​sleep 1​​,并​​echo​​出当前循环对应的数字。

注意:

  1. 真实的使用场景下,循环次数不一定等于10,或高或低,具体取决于实际的需求。
  2. 真实的使用场景下,循环体内执行的语句往往比较耗费系统资源,或比较耗时等。

请根据真实场景的各种情况理解本文想要表达的内容

$ cat test1.sh  
#/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"


通过上述代码可知,为了体现执行的时间,将循环体开始前后的时间打印了出来。

运行结果:

$ sh test1.sh 
1
2
3
4
5
6
7
8
9
10
startTime: 193649
endTime: 193659


10次循环,每次sleep 1秒,所以总执行时间10s。

方案

方案1:使用"&"使命令后台运行

在linux中,在命令的末尾加上​​&​​符号,则表示该命令将在后台执行,这样后面的命令不用等待前面的命令执行完就可以开始执行了。示例中的循环体内有多条命令,则可以以​​{}​​括起来,在大括号后面添加​​&​​符号。

$ cat test2.sh 
#/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"


运行结果:

sh test2.sh 
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​​命令,该命令等待当前脚本进程下的子进程结束,再运行后面的语句。

$ cat test3.sh 
#/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"


运行结果:

$ sh test3.sh 
1
2
3
4
5
6
7
9
8
10
startTime: 194221
endTime: 194222


但这样依然存在一个问题:

因为​​&​​使得所有循环体内的命令全部进入后台运行,那么倘若循环的次数很多,会使操作系统在瞬间创建出所有的子进程,这会非常消耗系统的资源。如果循环体内的命令又很消耗系统资源,则结果可想而知。

最好的方法是并发的进程是可配置的。

方案3:使用文件描述符控制并发数

$ cat test4.sh 
#/bin/bash

all_num=10
# 设置并发的进程数
thread_num=5

a=$(date +%H%M%S)


# mkfifo
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

# 关闭fd6管道
exec 6>&-

b=$(date +%H%M%S)

echo -e "startTime:\t$a"
echo -e "endTime:\t$b"


运行结果:

$ sh test4.sh 
1
3
2
4
5
6
7
8
9
10
startTime: 195227
endTime: 195229


方案4:使用​​xargs -P​​控制并发数

xargs命令有一个​​-P​​参数,表示支持的最大进程数,默认为1。为0时表示尽可能地大,即​​方案2​​的效果。

$ cat test5.sh 
#/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"


运行结果:

$ sh test5.sh 
1
2
3
4
5
6
8
7
9
10
startTime: 195257
endTime: 195259


方案5:使用​​GNU parallel​​命令控制并发数

​GNU parallel​​命令是非常强大的并行计算命令,使用​​-j​​参数控制其并发数量。

$ cat test6.sh 
#/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"


运行结果:

$ sh test6.sh 
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,实际编写时可以将该值作为一个参数传入。

相关知识点

  • wait命令
  • ​&​​后台运行
  • 文件描述符、mkfifo等
  • xargs命令
  • parallel命令