• 如何实现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 1 1" &>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 1 1 " 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 " 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:	$a"
    echo -e "endTime:	$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:	$a"
    echo -e "endTime:	$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:	$a"
    echo -e "endTime:	$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:	$a"
    echo -e "endTime:	$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:	$a"
    echo -e "endTime:	$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:	$a"
    echo -e "endTime:	$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脚本方案:任务切割、各个击破。

    先来 1 段场景描述:

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

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

    简单思路:

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

    具体代码:

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

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

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

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

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

     
     
     
     
     
  • 相关阅读:
    八步详解Hibernate的搭建及使用
    Hibernate的介绍
    javascript的优缺点和内置对象
    过滤器有哪些作用?
    JSP中动态INCLUDE与静态INCLUDE的区别?
    jsp的四种范围?
    jsp有哪些动作作用分别是什么?
    介绍在JSP中如何使用JavaBeans?
    jsp有哪些内置对象作用分别是什么 分别有什么方法?
    request.getAttribute() 和 request.getParameter() 有何区别?
  • 原文地址:https://www.cnblogs.com/timssd/p/6725897.html
Copyright © 2020-2023  润新知