Shell模拟多线程

UPDATE 20180605: 此种方式存在丢任务的情况,用 parallel 命令做多线程更好更简单

shell不能实现多线程,但是可以通过限制几乎同时放入后台执行的进程数量来模拟多线程,从而达到在提高脚本执行效率的同时又不明显增加负载的作用。

Ping脚本的多线程实现

#!/bin/bash
set -x	#开启调试模式
# Usage:
# History:
#
thread=$1	#设置线程数,在这里所谓的线程,其实就是几乎同时放入后台(使用&)执行的进程。
if [ "$1"x == ""x ]; then
	thread=1
fi

tmp_fifofile=/tmp/$$.fifo		#脚本运行的当前进程ID号作为文件名
mkfifo $tmp_fifofile			#新建一个随机fifo管道文件
exec 6<>$tmp_fifofile			#定义文件描述符6指向这个fifo管道文件
rm $tmp_fifofile			#清空管道内容

#定义一个函数做为线程(子进程),该函数功能是ping测试
function func()
{
	ping -c 3 $ip &>/dev/null && r=0 || r=1
	if [ $r -eq 0 ]; then
		echo "$ip ok"
	else
		echo "$ip failed"
	fi
	sleep 3
}

# for循环 往 fifo管道文件中写入$thread个空行
for [1]i=0;i<$thread;i++;do
	echo 
done >&6

# 从ip.txt中读取ip
while read ip;do
	read -u6		#从文件描述符6中读取行(实际指向fifo管道)
	{
		func		
		echo >&6	#再次往fifo管道文件中写入一个空行
	} &
# {} 这部分语句被放入后台作为一个子进程执行,所以不必每次等待3秒后执行
#下一个,这部分的func几乎是同时完成的,当fifo中thread个空行读完后 while循环
# 继续等待 read 中读取fifo数据,当后台的thread个子进程等待3秒后,按次序
# 排队往fifo输入空行,这样fifo中又有了数据,while循环继续执行

done < ip.txt		#从ip.txt中读取数据


wait			#等到后台的进程都执行完毕
exec 6>&-		##删除文件描述符6
exit 0

ip.txt中有9个ip,9个线程,调试模式执行结果

[root@localhost multhread]# ./thread.sh 9
+ thread=9
+ '[' 9x == x ']'
+ tmp_fifofile=/tmp/12088.fifo
+ mkfifo /tmp/12088.fifo
+ exec
+ rm /tmp/12088.fifo
+ [2] i=0 
+ [3] i<9 
+ echo
+ [4] i++ 
+ [5] i<9 
+ echo
+ [6] i++ 
+ [7] i<9 
+ echo
+ [8] i++ 
+ [9] i<9 
+ echo
+ [10] i++ 
+ [11] i<9 
+ echo
+ [12] i++ 
+ [13] i<9 
+ echo
+ [14] i++ 
+ [15] i<9 
+ echo
+ [16] i++ 
+ [17] i<9 
+ echo
+ [18] i++ 
+ [19] i<9 
+ echo
+ [20] i++ 
+ [21] i<9 
+ read ip
+ read -u6
+ func
+ ping -c 3 10.217.13.1
+ read ip
+ read -u6
+ func
+ ping -c 3 10.217.13.2
+ read ip
+ read -u6
+ func
+ ping -c 3 10.217.13.3
+ read ip
+ read -u6
+ func
+ ping -c 3 10.217.13.4
+ read ip
+ read -u6
+ func
+ ping -c 3 10.217.13.5
+ read ip
+ read -u6
+ func
+ ping -c 3 10.217.13.6
+ read ip
+ read -u6
+ func
+ ping -c 3 10.217.13.7
+ read ip
+ read -u6
+ func
+ ping -c 3 10.217.13.8
+ read ip
+ read -u6
+ func
+ ping -c 3 10.217.13.9
+ read ip
+ wait
+ r=0
+ '[' 0 -eq 0 ']'
+ echo '10.217.13.1 ok'
10.217.13.1 ok
+ sleep 3
+ r=1
+ '[' 1 -eq 0 ']'
+ echo '10.217.13.2 failed'
10.217.13.2 failed
+ sleep 3
+ r=1
+ '[' 1 -eq 0 ']'
+ echo '10.217.13.3 failed'
10.217.13.3 failed
+ sleep 3
+ r=1
+ '[' 1 -eq 0 ']'
+ echo '10.217.13.4 failed'
10.217.13.4 failed
+ sleep 3
+ r=1
+ '[' 1 -eq 0 ']'
+ echo '10.217.13.8 failed'
10.217.13.8 failed
+ sleep 3
+ r=1
+ '[' 1 -eq 0 ']'
+ echo '10.217.13.9 failed'
10.217.13.9 failed
+ sleep 3
+ r=1
+ '[' 1 -eq 0 ']'
+ echo '10.217.13.5 failed'
10.217.13.5 failed
+ sleep 3
+ r=1
+ '[' 1 -eq 0 ']'
+ echo '10.217.13.7 failed'
10.217.13.7 failed
+ sleep 3
+ r=1
+ '[' 1 -eq 0 ']'
+ echo '10.217.13.6 failed'
10.217.13.6 failed
+ sleep 3
+ echo
+ echo
+ echo
+ echo
+ echo
+ echo
+ echo
+ echo
+ echo
+ exec
+ exit 0

执行时间对比

[root@localhost multhread]# time ./thread.sh &>/dev/null

real    0m53.051s
user    0m0.004s
sys     0m0.020s
[root@localhost multhread]# time ./thread.sh 10 &>/dev/null

real    0m6.024s
user    0m0.013s
sys     0m0.016s
[root@localhost multhread]# time ./thread.sh 100 &>/dev/null

real    0m6.027s
user    0m0.015s
sys     0m0.017s
[root@localhost multhread]# time ./thread.sh 9 &>/dev/null

real    0m6.023s
user    0m0.013s
sys     0m0.015s

可以看到线程数量正好合适时执行速度比较快。

CMDB内外网错误修正脚本多线程实现

这是工作中的一个实例,我司的IP分为内网和外网,记录在CMDB中,坑爹的CMDB不校验内外网,可以随便填,于是各种乱象,内网写成外网的,外网写成内网的,还有写“内网IP”,“公网”,还有空着啥都不写的。

CMDB中记录的格式是 :ID,对象类型,IP地址,所属机器盘点号,内外网区分,描述,可以导出为csv文件。基于一个规则文件处理导出的csv数据,找出错误的数据,并纠正,然后在导入CMDB系统。

代码实现

#!/bin/bash

# Usage:
# History:
#


thread=$1        #设置线程数,在这里所谓的线程,其实就是几乎同时放入后台(使用&)执行的进程。
if [ "$1"x == ""x ] || [ "$2"x == ""x ]; then
        echo "2 args: ./cmdb.sh thread cmdbfile"
        exit 0
fi

CMDB_FILE_NAME=$2
RULES_FILE=rule.txt

rm -rf error correct no_rule
mkdir error
mkdir correct
mkdir no_rule

tmp_fifofile=/tmp/$.fifo                #脚本运行的当前进程ID号作为文件名
mkfifo $tmp_fifofile                        #新建一个随机fifo管道文件
exec 6<>$tmp_fifofile                        #定义文件描述符6指向这个fifo管道文件
rm $tmp_fifofile                        #清空管道内容

#定义一个函数做为线程(子进程)
function func()
{
        id=`echo $id | sed "s/\"\",/\"kong\",/g"`        #将类型为空的替换为 kong
        TYPE=`echo $id | awk -F '["]' '{print $10}'`                #cmdb中查到的网络类型,必须处理为空的类型
        NET=`echo $id | awk -F '["]' '{print $6}' |awk -F '[.]' '{print $1}'`        #IP地址前8位
        if [ "$NET"x = "10"x ]; then
                NET=`echo $id | awk -F '["]' '{print $6}' |awk -F '[.]' '{print $1"."$2}'`        #10开头的取前16位
                if [ "$NET"x = "10.62"x ] || [ "$NET"x = "10.63"x ] || [ "$NET"x = "10.64"x ]; then
                        NET=`echo $id | awk -F '["]' '{print $6}' |awk -F '[.]' '{print $1"."$2"."$3}'`        #10.62/63/64开头的取前24位
                fi
        fi
        
        [22]++$i
        echo "$i - $NET - $TYPE"
        
        RULE=`grep "^$NET\." $RULES_FILE | awk '{print $2}'`        #对应规则
        if [ "$RULE"x = ""x ]; then
                TMP=`echo $NET | awk -F '[.]' '{print $1}'`
                if [ "$TMP"x = "10"x ]; then
                        RULE=$TYPE
                        echo $id >>no_rule/$NET.csv
                else
                        RULE="外网"                                #规则中没有包含的且不为私有地址的统一作为外网处理,因此公网IP规则不需要写进规则文件
                fi
        fi
                
        if [ "$RULE"x != "$TYPE"x ]; then                #如果查到的类型和对应规则不符,则输出
                echo $id >>error/$NET.csv
                echo $id |sed "s/$TYPE/$RULE/g" >>correct/$NET.csv
        fi

        sleep 3
}

# for循环 往 fifo管道文件中写入$thread个空行
for [23]i=0;i<$thread;i++;do
        echo 
done >&6


#从cmdb.csv中读取
i=1
while read id;do
        read -u6                #从文件描述符6中读取行(实际指向fifo管道)
        {
                func                
                echo >&6        #再次往fifo管道文件中写入一个空行
        } &
# {} 这部分语句被放入后台作为一个子进程执行,所以不必每次等待3秒后执行
#下一个,这部分的func几乎是同时完成的,当fifo中thread个空行读完后 while循环
# 继续等待 read 中读取fifo数据,当后台的thread个子进程等待3秒后,按次序
# 排队往fifo输入空行,这样fifo中又有了数据,while循环继续执行
[24]i++
done < $CMDB_FILE_NAME                #从cmdb file中读取数据


wait                        #等到后台的进程都执行完毕
exec 6>&-                ##删除文件描述符6
exit 0

优化方案

上面的代码可以解决问题,但是速度太慢了,大约要30分钟。数据总量近10万条,错误的占总数并不多,但是脚本要一条条的去检查然后比对规则。因此如果能把错误的先找出来,在用上面的脚本处理几千条错误的,速度就能快很多。

改用grep,加-v选项,能实现错误的秒级查找,然后用上面的脚本纠错,也是几秒钟的事情,整个过程不到1分钟就能完成。

#!/bin/bash

# Usage:
# History:
#
set -x

if [ $# != 2 ];then
        echo "args error"
        exit 1
fi

rm -f error.csv
touch error.csv
rm -rf tmp
mkdir tmp

RULEFILE="$1"
CMDBFILE="$2"

id=1
cp $CMDBFILE tmp/tmp_$id

while read line
do
        NET=`echo $line |awk '{print $1}' |sed 's/\./\\\./g'`
        RULE=`echo $line |awk '{print $2}'`
        if [ "$NET"x = ""x ]; then
                NET="NULLOFRULE"
        fi
        NET="\\\"$NET"                        
        RULE="\\\"$RULE\\\","                #逗号必加,处理将内网网写到描述中去的情况
        grep -E ".+$NET.+" $CMDBFILE |grep -E -v ".+$NET.+$RULE.*" >> error.csv
        grep -E -v "$NET" tmp/tmp_$id >tmp/tmp_file
        [25]id++
        mv tmp/tmp_file tmp/tmp_$id
done <$RULEFILE

grep -E "\"10\." tmp/tmp_$id >no_rules.csv
grep -E -v "\"10\." tmp/tmp_$id |grep -E -v ".+\"外网\".*" >public_error.csv

参考资料

[1]. SHELL模拟多线程脚本的详细注解.http://blog.sina.com.cn/s/blog_65d6476a01017t7f.html
[2]. 管道技巧-while read line.http://blog.csdn.net/hunanchenxingyu/article/details/9998089
[3]. Linux shell 实现多线程. http://llystar.iteye.com/blog/1189486

 

参考资料

4 thoughts on “Shell模拟多线程

  1. 例子
    ```
    #!/bin/bash

    chan=$(mktemp -u /tmp/para.$$.XXXXXXXXX)
    mkfifo $chan
    exec 6<>$chan
    rm $chan

    task() {
    echo "$(date) - Start task $1"
    sleep 3
    }

    for i in {0..5};do
    echo
    done >&6

    for i in {0..10};do
    read -u6
    {
    task $i
    echo >&6
    } &
    done

    wait
    exec 6>&-
    ```

    • 执行结果
      ```
      Fri Jul 16 02:03:59 CST 2021 - Start task 2
      Fri Jul 16 02:03:59 CST 2021 - Start task 0
      Fri Jul 16 02:03:59 CST 2021 - Start task 1
      Fri Jul 16 02:03:59 CST 2021 - Start task 3
      Fri Jul 16 02:03:59 CST 2021 - Start task 4
      Fri Jul 16 02:04:00 CST 2021 - Start task 5
      Fri Jul 16 02:04:03 CST 2021 - Start task 6
      Fri Jul 16 02:04:03 CST 2021 - Start task 7
      Fri Jul 16 02:04:03 CST 2021 - Start task 10
      Fri Jul 16 02:04:03 CST 2021 - Start task 8
      Fri Jul 16 02:04:03 CST 2021 - Start task 9
      ```

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注