转载
http://blog.51cto.com/chenx1242/1757325

简单的多进程实现

首先,多进程和多线程不是一回事儿,这里说的是多进程。

俗话说的好,千言万语不如一个例子,于是我们先从一个例子开始,#vim 单进程.sh。

wKioL1b35R7CirkKAAAiaMErSNI368.png

:wq保存退出之后,#date ; sh 单进程.sh ;date。效果如下:

wKioL1b35g_D5ofAAAA3uSbQkHw356.png

算一下时间差,可以看到这个程序一共耗时15秒,跟程序里预设计的一模一样,程序里的i是从0~4五位数,每一个数停3秒,一共5 * 3=15秒。

例子里是需要执行相同的任务5次,等15秒钟也不是什么大不了的事儿。但是在实际生产里往往要重复执行几百上千次任务,如果这样的等来等去,脚本执行完了黄花菜也凉了。于是就要引进多进程,使用了多进程之后,在上面那个例子里就不再是"执行任务1然后执行2....最后执行任务5"这样线性启动任务,而是"同时执行任务1、任务2...任务5"这样的并行执行任务。

多进程其实很简单。只需要在上面的程序里更改两处地方:

wKioL1b36DTi0j5_AAAjscZ4qhY902.png

改过的脚本比原来的脚本多了一个&,可见整个{}内容转移到后台去运行,同时我这里在done后面加上一个wait,其实如果没有后续命令的话,这个wait是可以省略的。如果有后续命令的话,wait要加上,代表确定上面的所有程序都完成之后,才会执行“后续命令"。

现在执行一下看一下时间消耗:

wKiom1b35_TwddYkAAAmDi2b_NQ875.png

只走了3秒钟,也就是一个sleep 3,可见效果立竿见影。

限制进程数量的多进程实现

可见用了多进程真的是很爽,压缩了大量的时间。但是又有一个问题,如果都按上面的例子写脚本,那么5个任务同时执行5个进程,10个任务就同时执行10个进程。那要是200个任务岂不是要同时执行200个进程?500个任务岂不是执行500个进程?那服务器又不是无敌铁金刚,一下子开这么多进程肯定受不了。

于是就要“控制进程数量”,但是linux没有一个专门限制进程数量的变量和命令,怎么办呢?可以先用一个for语句做一个shell,而这个小shell可以在目标程序里来回的启动若干次,这样达到一个"大shell套小shell”编程思路,而这个for语句的shell就是进程数量。但是要在真正写脚本之前先理解两个定义,管道和文件操作符。

| 就是一个管道,只不过这个管道是无名的。就像我们平时常用的#cat .log | grep ABC 这个语句里的|就是管道了前面了cat .log命令。

但是我们这里要用的是有名管道,mkfifo。mkfifo命令就可以创建一个管道文件,#mkfifo BBB,就这样建立了一个管道文件叫BBB。管道文件是"边进边出"的,就是说仅仅是输入内容的话,那么程序会卡住。比如这样:

wKioL1b37MSAlUWzAAAd4RTX7uE184.png

如图可见,程序就在这里进入等待状态,等待什么呢?等待有一个程序在输出这个BBB,不然就会一直等待下去,于是重开一个服务端并且到该目录下执行#cat BBB,于是看到:

wKiom1b37SCCYWtIAAAcD6C3SIg525.png

而且第一个服务器界面的程序也终止了等待状态。

上面这个例子是“先进后出”,如果要是“先出后进",也是一样的。可见管道文件就像一个水管一样,里面不存储文件,有"水"进去就必须让"水"出来,不然管道程序就会一直处于等待状态。而这种先出后进的方法再写shell的时候更常见。

现在说文件操作符,linux下的文件描述符是与文件输入、输出相关联的整数。它们用来跟踪已打开的文件。最常见的文件描述符是stdin、stdout和stderr.我们可以将某个文件描述符的内容重定向到另一个文件描述符中。

文件描述符0、1以及2是系统预留的。
0——stdin(标准输入)
1——stdout(标准输出)
2——stderr(标准错误)

012的描述符都不能再赋予其他意义,于是可以自己新建一个新的文件描述符。然后搭配exec命令,还是那句话,千言万语不如一个例子,直接上代码:

!/bin/bash

trap "exec 1000>&-;exec 1000<&-;exit 0" 2

"exec 1000>&-;exec 1000<&1000-"这个是关闭文件操作符1000的意思,后面的2指的是ctrl+C的操作

                                            #也就是将文件处于一个初始化的状态

mkfifo testfifo #建立管道文件
exec 1000<>testfifo #exec 1000<>testfifo指的是把文件操作符1000与testfifo绑定的意思,对1000的操作>既是对testfifo的操作
rm -fr testfifo #用完管道文件就要删除
for ((n=1;n<=10;n++)) #多进程说白了就是大shell里面套一个小shell,而这个小shell就是进程数,这里进程>数是10

    do
            echo >&1000       #将“空白”输入到1000文件操作符即输入到testfifo里
    done             #管道读取是以行为单位的,所以要输入行而不是字

start=$(date) #显示一下当前时间
echo $start
for ((i=1;i<=50;i++)) #这个是正式的任务,一共有50项,上面定了10进程数,所以每次完成5个任务。
do
read -u1000
#通过read -u把大小shell连接起来,一次读取1000里的一个空行,全部读取完毕之后,没有其他空行了,任务就停止了

    {
    echo 成功$i;sleep 5
    echo >&1000
    }&

done
wait
end=$(date) #再显示一下当前时间,计算一下时间差
echo $end
exec 1000>&-;exec 1000<&- #和第一句遥相呼应

结合这个脚本就应该很明白的理解多进程其实就是“大shell套小shell”的说法了。执行这个脚本之后,就会看到结果就是一共50个任务(脚本里的i),10个为一捆(脚本里的n),分为5次执行完毕,一共是5 * 5=25秒。

read -u的拓展

上面的程序里用到了read -u1000,这里指的是每一次read -u1000就是执行一次,然后从1000号的fd(即testfile)里提取一行(例子中是空白行),然后向下执行,当没有行的时候,整个任务就停止。

而read -u 后面是可以接变量的,比如read -u3 i,这个语句意思就是"从3号fd里提取一行,同时这一行数据的变量叫i",同理read -u4 j的意思就是"从4号fd里提取一行,这一行数据是变量j"。

比如有这样的一个脚本,

!/bin/bash

written by ChrisChan @ 2016-3-23

while read -u1001 i &&read -u1002 j;

    do
    {
    echo $i $j
    }&
    done  1001<a.txt   1002<b.txt

wait
exec 1001>&-;exec 1001<&-
exec 1002>&-;exec 1002<&-

这一段也是很标准的"管道文件先出后进",先指定1001和1002文件标识符与那个管道文件相关联,然后输出,如果a.txt的内容是以下四行:

姚明 篮球运动员
刘翔 短跑运动员
迈克尔杰克逊 流行歌手
梅西 足球运动员

而b.txt的内容是六行的文字:

A
B
C
D
E
F

那么整个程序就会有这样的输出:

梅西 足球运动员 D
迈克尔杰克逊 歌手 C
刘翔 短跑运动员 B
姚明 篮球运动员 A

可见进行了一一对应,而且自动按照字母顺序排列好。

而且要注意,while语句执行完毕之后,1001和1002这两个文件描述符就没有了,在同脚本的后面也不会被启动,如果要保留这两个变量给下面的语句使用的话,那么需要使用exec 1001<a.txt。