背景:
遇到一个业务需求,一个上位机需要向多个下位机传送文件,当前的实现是for循环遍历所有下位机,传送文件,但是此种方法耗时太久,需要优化。因此可以通过并发的方式向下位机传送文件。
这边写一段测试代码,就简单输出一些内容。(输出1-10数字)
注意:以下每个点都是依次递进的。
实现过程:
1. 通过串行的方式实现
遍历1-10,然后将其输出(中间每个操作休眠1s,最终耗时10s)
# 串行执行命令
echo -e "串行执行:"
all_num=10
a=$(date +%H%M%S)
for ((i=1;i<=${all_num};i++))
do
{
sleep 1
echo ${i}
}
done
b=$(date +%H%M%S)
echo -e "startTime:\t$a"
echo -e "endTime:\t$b"
结果:
当前问题:
输出1-10,是10个循环,是按照顺序执行的,每一个循环1s,那么10个循环是10s,那如果需要输出1-1000呢?就是需要1000s,效率过于低下,所以需要并发执行。
2. 使用&,以及wait并发执行
知识储备:
符号&:在命令的末尾加上&,表示该命令在后台执行,后面的命令就无需等待这条命令执行完就可以执行。
wait:将wait之前的命令都执行结束(当前进程下的子进程都执行结束),才开始wait之后后面的语句。
代码:(耗时1s)
# 2.并发执行(&符号表示{}内的命令将在后台执行,后面的命令不用等前面的命令执行完就可以执行了)
# (wait是将当前脚本进行下的子进程都执行结束,然后在执行后面的语句)
echo -e "并发执行:"
all_num=10
a=$(date +%H%M%S)
for ((i=1;i<=${all_num};i++))
do
{
sleep 1
echo ${i}
}&
done
wait
b=$(date +%H%M%S)
echo -e "startTime:\t$a"
echo -e "endTime:\t$b"
结果:
由此可以发现,并发执行确实在时间上优化了很多,但是有需要考虑一个问题:如果现在有1000个任务,那么后台就需要并发1000个任务(一下创建出1000个子进程),这样对系统造成非常大的压力,并发任务数量增多,操作系统的处理速度就会变慢,甚至出现其他不稳定因素。所以,最好是可以控制并发数(控制子进程的数量)。
3. 使用文件描述符和管道控制并发数
知识储备:
管道特性:管道默认是阻塞的,当进程从管道中读取数据,如果没有数据则进程会阻塞;
当一个进程往管道中不断地写入数据但是没有进程去读取数据,此时只要管道没有满是可以写的,但若管道放满数据的则会报错。
有名管道:它是一种文件类型,在文件系统中可以看到。
利用有名管道的上述特性就可以实现一个队列控制了。
你可以这样想:一个公共厕所总共就10个蹲位,这个蹲位就是队列长度,厕所门口放着10把钥匙,要想上厕所必须拿一把药匙,上完厕所后归还药匙,下一个人就可以拿药匙进去上厕所了,这样同时来了1000个人上厕所,那前十个人抢到药匙进去上厕所了,后面的990人需要等一个人出来归还药匙才可以拿到药匙进去上厕所,这样10把药匙就实现了控制1000人上厕所的任务(os中称之为信号量)。
管道具有存一个读一个,读完一个就少一个,没有则阻塞,放回的可以重复取,这正是队列特性,但是问题是当往管道文件里面放入一段内容,没人取则会阻塞,这样你永远也没办法往管道里面同时放入10段内容(想成10把药匙),解决这个问题的关键就是文件描述符了。
创建有名管道文件,创建文件描述符关联管道文件,这时候这个文件描述符就拥有了管道的所有特性,还具有一个管道不具有的特性:无限存不阻塞,无限取不阻塞,而不用关心管道内是否为空,也不用关心是否有内容写入引用文件描述符: &2可以执行n次echo >&2 往管道里放入n把钥匙
代码:
# 3.并发执行(&符号表示{}内的命令将在后台执行,后面的命令不用等前面的命令执行完就可以执行了)
# 控制并发数量(根据自己的当前业务需求)
echo -e "并发执行(控制并发数量,利用管道,文件描述符):"
all_num=10
# 并发的进程数
thread_num=5
a=$(date +%H%M%S)
# mkfifo 创建有名管道
myfifo="fd1"
mkfifo=${myfifo}
# 创建文件描述符,以可读(<)和可写(>)的方式关联管道文件,这时候文件描述符2就有了有名管道文件的所有特性
# 关联之后的文件描述符拥有管道文件的所有特性,所以可以将管道文件进行删除,之后使用文件描述符2即可
exec 2<>${myfifo}
rm -f ${myfifo}
# 为文件描述符创建占位信息(&2表述文件描述符2,往管道里面放入了一个“令牌”,相当于最多可以开启有thread_num个进程)
for ((i=1;i<=${thread_num};i++))
do
{
echo >&2
}
done
# 命令
for ((i=1;i<=${all_num};i++))
do
# 代表从管道中获取一个令牌(一共有thread_num个进程,现在使用一个进程,2是代表文件描述符)
read -u2
{
sleep 1
echo ${i}
# 代表执行完当前命令,将这个令牌又放入到管道中(使用完这个进程,将这个进程又返还回去)
echo >&2
}&
done
wait
# 命令执行完成,关闭文件描述符的读写
exec 2<&-
exec 2>&-
b=$(date +%H%M%S)
echo -e "startTime:\t$a"
echo -e "endTime:\t$b"
结果:
这种方法没有第二种方法快,但是比方法一块,这样是既可以提高效率,又实现了并发控制。
4. 使用xargs -P控制并发数
知识储备:
-I:执行多条命令(具体使用请查下,这里只是简单普及) -I后面的{},输出是sh -c中的内容,它代表seq 1 ${all_num},即1-10
-n:指定每行字符数
-P:表示支持的最大进程数,默认为1。为0时表示尽可能地大
代码:
# 4.并发执行(使用xargs -P控制并发数)
all_num=10
thread_num=5
a=$(date +%H%M%S)
# cp -rf /root/result /root/test-patch/ | xargs -n 1 -I {} -P ${thread_num} sh -c
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"
结果:
5. 使用parallel控制并发数
知识储备:
-j 是控制并发数(command1 ::: arg1 arg2 arg3)。
并行执行command1命令,并使用:::符号传递arg1、arg2和arg3参数。
代码:
# 5.并发执行(使用parallel控制并发数)
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"
结果:因为当前电脑没有安装parallel ,就不贴结果了。