如何实现shell并发 一个入门级可控多线程shell脚本方案

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

参考文章

  1. http://blog.csdn.net/qq_34409701/article/details/52488964
  2. https://www.codeword.xyz/2015/09/02/three-ways-to-script-processes-in-parallel/
  3. http://www.gnu.org/software/parallel/

相关知识点

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

一个入门级可控多线程shell脚本方案

说到shell可控多线程,网上分享的大部分是管道控制的方案。这种方案,张戈博客也曾经实战并分享过一次:《Shell+Curl网站健康状态检查脚本,抓出中国博客联盟失联站点》,感兴趣的朋友可以看看。

如何实现shell并发 一个入门级可控多线程shell脚本方案

下面张戈博客再分享另一种更容易理解的入门级可控多线程shell脚本方案:任务切割、各个击破。

先来 1 段场景描述:

某日,在鹅厂接到了这个任务,需要在Linux服务器中,对几千个IP进行一次Ping检测,只要取得ping可达的IP就好。如果单个IP去ping测试,虽然也可以完成任务,几千个IP还好了,如果更多呢?

鉴于这个case简单程度,第一时间先放弃了以前用过的管道方案,而是采用了各个击破的思想。

简单思路:

按照任务切割的“战略思想”,我先将这几千IP存入一个iplist文件,然后写一个分割函数,将这个文件分成多份临时IP清单,最后,用多线程遍历这些临时IP文件即可变相实现多线程了。

具体代码:

 
 
 
 

Shell

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/sh
#文本分割函数:将文本$1按份数$2进行分割
SplitFile()
{
    linenum=`wc -l $1 |awk '{print $1}'`
    if [[ $linenum -le $2 ]]
    then
        echo "The lines of this file is less then $2, Are you kidding me..."
        exit
    fi
    Split=`expr $linenum / $2`
    Num1=1
    FileNum=1
    test -d SplitFile || mkdir -p SplitFile
    rm -rf SplitFile/*
    while [ $Num1 -lt $linenum ]
    do
        Num2=`expr $Num1 + $Split`
        sed -n "${Num1}, ${Num2}p " $1 > SplitFile/$1-$FileNum
        Num1=`expr $Num2 + 1`
        FileNum=`expr $FileNum + 1`
    done
}
 
#Define some variables
SPLIT_NUM=${1:-10} #参数1表示分割成多少份即,开启多少个线程,默认10个
FILE=${2:-iplist}  #参数2表示分割的对象,默认iplist文件
 
#分割文件
SplitFile $FILE $SPLIT_NUM
 
#循环遍历临时IP文件
for iplist in $(ls ./SplitFile/*)
do
    #循环ping测试临时IP文件中的ip(丢后台)
    cat $iplist | while read ip
    do
        ping -c 4 -w 4 $ip >/dev/null && echo $ip | tee -ai okip.log #ping 可达的IP则写入日志
    done &     #在while循环后面加上&符号,让这个嵌套循环在后台执行
done

将代码保存为ping.sh之后,执行  sh ping.sh  iplist 100 的过程如下:

先将iplist切割成100份,存放在 SplitFile 文件夹中

然后,通过for循环读取这些分割文件,并在后台使用while循环对其中ip执行ping命令。

由于while是丢后台的, 所以for循环会一次性执行100个while,相当于开启了100个线程,速度自然不可同日而语矣。

其中,切割的份数即你想要开启的多线程数量,很明显,这种任务分割的思路虽然没有管道方案来的高大上,但是其思想更加简单易懂,而且通用性也更好,适合入门级的简单多线程任务。

 
 
 
 
 
上一篇:shell模拟“多线程”


下一篇:jQuery源码笔记——延迟对象