Linux shell实现多进程(进程池)
最近有个需求是要根据一个给定的 img.txt 文本批量下载一批文件,文本中每一行都是一个文件下载链接,样式如下: img.txt
http://www.test.com/download/a.zip
http://www.test.com/download/b.zip
http://www.test.com/download/c.zip
...
根据需求最开始我打算写个脚本直接for循环+wget下载,脚本如下: 版本 1 (串行执行)
#!/bin/bash
for url in $(cat img.txt); do
echo "download: ${url}"
wget ${url}
done
echo "you have download all files"
实际下载时我发现这样做效率太低,因为for循环是单进程串行执行的,同一时间只能下载一个文件,如果这些下载地址中出现一个特别大的文件,那么后面所有的文件都要等待这个特别大的文件下载完成之后才能下载,耗时太久,怎么使用多进程同时下载这些文件呢
主要参考了这位博主:
https://blog.csdn.net/helimin12345/article/details/107592084
如果将一堆语句用{}括起来,在末尾加一个 &,那么shell就会启动一个子进程去执行{}里面的内容,而 wait 命令可以阻塞当前线程,等待这些子进程全部执行完之后再执行剩下的语句,于是改进脚本如下: 版本 2 (并行执行)
#!/bin/bash
for url in $(cat img.txt); do
{
echo "download: ${url}"
wget ${url}
} &
done
wait
echo "you have download all files"
改进后的脚本其实有问题,for循环每一次都会开启一个子进程,如果下载链接特别多的话,会同时启动大量的进程,消耗服务器大量资源。并且服务器CPU数量是有限的,进程太多,CPU在进程间的切换也会非常耗费时间,效率反而降低。一般来说,进程数量和逻辑CPU数量相等时效率较高,要怎么准确控制进程的数量呢
实现多进程一个比较好的方式是使用队列,可以使用rabbitmq,kafka,redis等消息队列中间件,不过为了写个脚本还要部署一套中间件特别麻烦。linux中自带就有一个mkfifo 命令可以创建出一个管道,如果往管道中写入数据,进程会被阻塞,直到有另一个进程从管道中读取数据,同样的如果从管道中读取数据而管道中没有数据时,进程也会被阻塞,直到有另一个进程向管道中写入数据。根据这个特点可以用来实现生产者消费者模型,但是实测会丢失数据,具体原因未知: 另起一个窗口从管道中读取数据 直接使用mkfifo创建的管道出现数据丢失,但如果使用 exec 命令给管道绑定文件描述符,再使用管道就不会出现数据丢失了 这里要注意0,1,2,255这四个文件描述符已经被操作系统用了,我这里用了4 实测另起一个窗口从管道中读取数据不会丢失,可以看出管道是先进先出的队列 补充一下:可以通过命令 ls -lh /proc/$$/fd 查看系统文件描述符 新建一个管道,添加文件描述符,向管道中事先写入几条数据,然后每开启一个子进程就读取1条数据,子进程结束前再向管道中补充1条数据,通过这种方式可以做到控制进程数量,脚本改进如下: 版本 3 (控制进程数量)
#!/bin/bash
mkfifo mylist
exec 4<>mylist
for ((i=0; i < 5; i++)); do
echo >mylist
done
for url in $(cat img.txt); do
read <mylist
{
echo "download: ${url}"
wget ${url}
echo >mylist
} &
done
wait
echo "you have download all files"
exec 4<&-
exec 4>&-
rm -f mylist
脚本写到这里功能就已经达到了,子进程数量被控制了。但仔细分析一下以上的代码其实已经可以实现生产者消费者模型了,为什么不直接使用管道在进程间传递数据呢?我的想法是把所有下载链接全部写入到管道,然后开启指定个数的子进程,每个子进程内部都不断从管道中读取数据,直到管道为空读取不到数据了再关闭子进程,原理如下:
根据以上原理我将脚本改进如下:
版本 4 (生产者消费者模型)
#!/bin/bash
mkfifo mylist
exec 4<>mylist
for ((i=0; i < 5; i++)); do
{
while read -t 1 url <mylist; do
echo "download: ${url}"
wget ${url}
done
} &
done
for url in $(cat img.txt); do
echo ${url} >mylist
done
wait
echo "you have download all files"
exec 4<&-
exec 4>&-
rm -f mylist
这里要注意的是必须先开启子进程,然后再将数据写入到管道。因为管道是有长度的,如果先将数据写入到管道,再开启子进程,如果数据量很大超过了管道长度,那么主进程会被一直阻塞,根本执行不到后面开启子进程。如果先开启子进程,再向管道文件中写入数据,数据一写入管道马上就被子进程读取消费,不会出现主进程被阻塞执行不了的问题。通过这样改进后子进程一直都是固定的,理论上效率会提高一点。但是实际测试发现有线程安全问题,多个子进程从同一个管道中读取数据时,可能会读取到同一份数据。解决该问题必须在子进程读取管道时加锁。可以通过再引入一个管道实现加锁解锁,最终脚本如下:
版本5 (解决线程安全问题)
#!/bin/bash
mkfifo mylist
exec 4<>mylist
mkfifo mylock
exec 5<>mylock
echo >mylock
for ((i=0; i < 5; i++)); do
{
while read -t 1 < mylock && read -t 1 url <mylist; do
echo >mylock
echo "download: ${url}"
wget ${url}
done
} &
done
for url in $(cat img.txt); do
echo ${url} >mylist
done
wait
echo "you have download all files"
exec 4<&-
exec 4>&-
rm -f mylist
exec 5<&-
exec 5>&-
rm -f mylock
完结,如果有更好的想法欢迎留言讨论
|