SHELL中并发控制

在很多情况下,我们使用单线程循环运行一些数量较大的小程序时耗时会较长,如下载10000个小文件(单线程下载很难达到宽带上限),这个时候就会考虑到多进程并发,即:将多个任务放到后台执行。但是在很多情况需要对线程的数目提出要求,本文就是讲解SHELL如何实现并发控制的。

一、前言

SHELL是不可能实现多线程的1,因为SHELL下面的每个命令实质上都是进程,但是可以通过多进程并发看起来像是多线程的操作。但是多进程并发会带来资源分配的问题,所以必须要控制进程数目(进程的创建所消耗的资要比线程多)。

实现SHELL的进程数目控制听起来比较简单,似乎只需要使用一个全局变量用于记录创建的数量即可。但是这样并不行,原因是:子进程虽然能够继承父进程的变量,但是却不会改变父进程的变量值(这也是为什么一台linux系统下的两个终端环境变量可以随设置,互不影响),所以SHELL编程并不存在这样的“全局变量”。当然如果将内容写到文件中去,将文件的内容充作“全局变量”,自然也是可以的。但是这样会存在一些问题,首先这样会造成频繁的IO读写,效率不高;其次文件读写速度远远低于内存读写速度,会出现同时多进程读写同一个文件的情况,会造成混乱(毕竟没有类似多线程并发的“锁”)

不过,LINUX下还是可以使用命名管道实现进程数目的控制,因为命名管道具有全局性,同时也不会出现读写混乱的问题,下面就具体说一说实现原理与方法2

二、基本并发实现

下面是一个非常简单的案例,就是输出5个Hellow World,每个Hellow World需要2s钟的时间,所以运行结果也是自然非常明了。

#!/bin/bash
start=`date +%s`
for i in `seq 5`;do
    echo "Hellow World"
    sleep 2
done
end=`date "+%s"`
echo "TIME:`echo "${end} - ${start}" | bc`"

运行结果:

Hellow World
Hellow World
Hellow World
Hellow World
Hellow World
TIME:10

为了实现5个Hellow World能够同时运行,自然只需要使用{}将相应的代码变成,然而在后面添加&,将其放到后台执行即可。但是为了保证主程序不会提前结束,还需要wait指令来等待所有后台程序结束。并发实现代码:

#!/bin/bash start=`date +%s`
for i in `seq 5`;do
    {
        echo "Hellow World"
        sleep 2
    }&
done
wait
end=`date "+%s"`
echo "TIME:`echo "${end} - ${start}" | bc`"

运行结果:

Hellow World
Hellow World
Hellow World
Hellow World
Hellow World
TIME:2

三、进程并发数目控制

显然,基本并发实现虽然实现了多进程并发的目的,但是却不能够控制后台的进程的数目。为了控制进程的数目,接下引入了管道文件描述符两个概念。

3.1 管道与文件描述符

管道分为有名管道无名管道两种。其中无名管道就是常见的|操作符,将上个命令的标准输出变成下个命令的标准输入。而mkfilo则可以创建一个有名管道。管道有一个特点:输入和输出必须同时存在才能正常执行,否则就会停止操作,而进行等待。

根据Linux一切文件的理论,管道本质上也是一个”文件”,而有名管道则实质上也是一个文件,却起着管道的作用。使用mkfilo可创建一个有名管道文件:

$ mkfilo fifo

不过这个文件虽然可以和正常的文件一样读取和写入,但是却有着管道的特性,如果这个时候向里面写入内容,而没有读取的话,就会”卡住”,如:

$ echo "test" > fifo

这个时候就会卡住,直到有程序接收fifo的输出为止。如何做呢?只需另开一个终端,readcat一下该文件即可。

#另开一个终端
$ cat fifo
test

显然管道的“阻塞”特性是并发所想要的,然而这种这种输入和输出必须同时存在否则程序就无法继续的特性却是并发过程所不想要的,单纯使用的管道无法解决该问题,这个时候需要绑定文件描述符

文件描述符指的是将一些整数与打开的文件相关联(根据linux一切皆文件想法,这些文件自然也可以是设备)。系统默认的是有标准输入、标准输出和标准错误,分别和整数0,1,2相关联。关于这点可以在/proc/self/df下查看。

$ ls -al /proc/self/fd
lrwx------ 1 aa aa 64  7月  5 19:31 0 -> /dev/pts/30
lrwx------ 1 aa aa 64  7月  5 19:31 1 -> /dev/pts/30
lrwx------ 1 aa aa 64  7月  5 19:31 2 -> /dev/pts/30

可出看出三个文件描述符都指向了当前的终端设备。

文件描述符可以通过exec命令进行绑定,即将一个文件和一个整数绑定起来。当然这个整数也是有大小也是限制的,可以通过ulimit -n来查看文件描述符的数量限制,一般情况下为1024,也就意味着文件描述符的整数取值范围为0~1023

将管道文件绑定文件描述符后,具有这样的一种特性:可以任意写入,也可以随意读取,在管道文件内部有内容的时候并不“阻塞”,如果试图读取空的管道文件的时候,就会“阻塞”,这样即可实现并发控制.

实现代码如下:

$ mkfifo fifo
$ exec 1000<>fifo
$ rm -fr fifo

第三步直接将管道文件删除看起来比较奇怪,但是实际上只是删除该文件标记,但是由于该文件已经被打开了(exec绑定到文件描述符1000上)。所以该管道文件占用空间依旧存在,将内容定向到1000上依旧可以写入,直到该文件被关闭后空间才会释放。

3.2 并发控制

利用上述文件描述符+管道实现并发控制的基本想法如下:

1. 将n行数据写入管道文件中
2. 并发执行任务,每执行一个任务就会读取管道文件的一行内容
3. 当每个任务结束就会行管道文件中写入一行内容
4. 当管道文件不存在内容的时候,就会"阻塞"

脚本如下:

#1. 将3行数据写入管道文件中(空行)
for i in `seq 3`;do
    echo >& 1000
done
for i in `seq 10`;do
    #2. 并发执行任务,每执行一个任务就会读取管道文件的一行内容
    #4. 当管道文件不存在内容的时候,就会"阻塞"
    read -u1000
    {
        echo "Hellow World"
        sleep 2
        #3. 当每个任务结束就会行管道文件中写入一行内容
        echo >&1000
    }&

这样就较好的实现了并发数目控制。

3.3 完整的实现

由于采用了exec的绑定,所以完整的shell代码应当将该文件及时的关闭。完整的代码如下:

# 接受信号 2 (ctrl +C)做的操作(防止程序中断而导致1000无法正常关闭)
trap "exec 1000>&-;exec 1000<&-;exit 0" 2

for i in `seq 3`;do
    echo >& 1000
done

start=`date +%s`
for i in `seq 10`;do
    read -u1000
    {
        echo "Hellow World"
        sleep 2
        echo >& 1000
    }&
done
wait

end=`date +%s`
echo "TIME: `echo ${end} - ${start} | bc`"

# 关闭操作,<>必须单独进行关闭
exec 1000>&-
exec 1000<&-

Reference

此条目发表在LINUX分类目录,贴了标签。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。