Shell脚本并发控制

本文遵循BY-SA版权协议,转载请附上原文出处链接。


本文作者: 黑伴白

本文链接: http://heibanbai.com.cn/posts/77cc6cb7/

Shell脚本并发控制

在编写Shell脚本时,有些内部逻辑需要通过并发来提高效率,但并发如何进行控制?下面进行下简单的介绍~

实战场景

举例一个简单的示例,假设要输出数字1到10,且每次输出前sleep 1即睡眠1秒钟

普通方案

根据场景描述,首先想到的普通方法就是for循环输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

# 开始时间
s_time=`date +%Y%m%d-%H%M%S`

# 业务代码
for num in `seq 1 10`
do
sleep 1
echo $num
done

# 结束时间
e_time=`date +%Y%m%d-%H%M%S`

echo -e "start_time:\t${s_time}"
echo -e "end_time:\t${e_time}"

执行脚本查看,输出10个数字,每次sleep 1,共耗时10秒:

1
2
3
4
5
6
7
8
9
10
11
12
13
[heibanbai@heibanbai ~]$ sh shell_for.sh
1
2
3
4
5
6
7
8
9
10
start_time: 20230129-223446
end_time: 20230129-223456

并行方案

方案1

在命令的末尾使用&符号,将命令放到后台执行,如果核心业务代码有多条命令,可以将其放到{}中,然后在大括号后添加&符号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

# 开始时间
s_time=`date +%Y%m%d-%H%M%S`

# 业务代码
for num in `seq 1 10`
do
{
sleep 1
echo $num
}&
done

# 结束时间
e_time=`date +%Y%m%d-%H%M%S`

echo -e "start_time:\t${s_time}"
echo -e "end_time:\t${e_time}"

执行脚本查看,发现先输出的时间然后才是数字,同时前后开始和结束时间一致且中间还有[heibanbai@heibanbai ~]$,这是因为通过&,业务代码全部进入了后台执行,脚本主进程在不到1秒钟的时间就已经执行完了

1
2
3
4
5
6
7
8
9
10
11
12
13
[heibanbai@heibanbai ~]$ sh shell_parallel_01.sh
start_time: 20230129-224432
end_time: 20230129-224432
[heibanbai@heibanbai ~]$ 6
7
3
4
5
8
9
10
1
2

那么如何控制上面出现的问题,在核心业务代码执行完成后再执行后面的代码呢?只需要通过wait命令即可实现,加上wait后,将会等待子进程执行结束后再执行后面的语句,修改后脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

# 开始时间
s_time=`date +%Y%m%d-%H%M%S`

# 业务代码
for num in `seq 1 10`
do
{
sleep 1
echo $num
}&
done

# 等待子进程执行结束
wait

# 结束时间
e_time=`date +%Y%m%d-%H%M%S`

echo -e "start_time:\t${s_time}"
echo -e "end_time:\t${e_time}"

执行脚本查看,在所有数字全部输出后才执行的后续代码,但仔细思量将会发现有一个问题,假如核心业务代码循环次数非常多且又消耗系统资源,那么将会出现什么情况?这就需要对并发进行控制,如何进行控制将通过下面的方法进行介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
[heibanbai@heibanbai ~]$ sh shell_parallel_02.sh
6
7
3
4
8
9
10
1
5
2
start_time: 20230129-225222
end_time: 20230129-225223

方案2

通过使用管道和文件描述符控制并发数量,这里大概说下两个概念

  • 管道

    有无名管道和有名管道两种

    无名管道,在日常使用频率超高,比如:

    1
    ls ./ | wc -l

    这里面的|就是管道,它将前一个命令的结果输出到后一个进程中,作为两个进程的数据通道,不过他是无名的。

    使用mkfifo命令创建的管道即为有名管道,例如mkfifo pipefilepipefile即为有名管道
    有名管道有一个特点:如果管道里没有数据,那么去取管道数据时,程序会阻塞住,直到管道内进入数据,然后读取才会终止这个操作,反之,管道在执行写入操作时,如果没有读取操作,同样会阻塞

    由此可以得到:利用有名管道的特性就可以实现一个队列的控制。

  • 文件描述符

    File Descriptors (FD,文件描述符或文件句柄):进程使用文件描述符来管理打开的文件。文件描述符是与输入和输出流相关联的整数。最广为人知的文件描述符是stdin(0)、stdout(1)和stderr(2),文件描述符01以及2是系统预留的。我们可以将某个文件描述符的内容重定向到另一个文件描述符中

    文件描述符的值不能乱取,取值范围是3至(ulimit -n)-1ulinit -n是 系统的open files,默认一般就是1024,直接输入ulimit -n就可以查看。一般使用范围是数字0~9,重定向时大于9的文件描述符要谨慎使用,因为可能与Shell内部使用的文件描述符冲突

    Bash的内部命令exec的功能之一是允许我们操作文件描述符。常用的打开模式有3种:

    • 只读模式。
    • 追加写入模式。
    • 截断写入模式。

    <操作符可以将文件读入stdin>操作符用于截断模式的文件写入(数据在目标文件内容被截断之后写入)。>>操作符用于追加模式的文件写入(数据被追加到文件的现有内容之后,而且该目标文件中原有的内容不会丢失)。文件描述符可以用以上3种模式中的任意一种来创建。

那么将上述场景进行改造后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/bin/bash

# 设置并发进程数
thread_num=5

# 开始时间
s_time=`date +%Y%m%d-%H%M%S`

# mkfifo 创建有名管道
tempfifo="temp_fifo"
mkfifo ${tempfifo}

# 创建文件描述符,以可读(<)可写(>)的方式关联管道文件,文件描述符6拥有有名管道文件的所有特性
exec 6<>${tempfifo}
# 删除管道文件,因为文件描述符打开的文件即使删除句柄也不会被释放
rm -f ${tempfifo}

# 为文件描述符创建占位信息
for ((i=1;i<=${thread_num};i++))
do
{
# 这个表示只写入回车 将命令的执行结果写入文件描述符6打开的文件 &6表示引用文件描述符6
echo >&6
}
done

for num in `seq 1 10`
do
{
# -u表示对文件描述符进行读取,如果能读取则执行下面的命令,如果不能则等待
read -u6
{
sleep 1
echo ${num}
# 由于之前是从管道文件中读走了一行,这里要再还回去一行,让后面的进程进行使用
echo >&6
} &
}
done

# 等待子进程执行结束
wait

# 释放文件描述符
exec 6>&-

# 结束时间
e_time=`date +%Y%m%d-%H%M%S`

echo -e "start_time:\t${s_time}"
echo -e "end_time:\t${e_time}"

执行脚本查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
[heibanbai@heibanbai ~]$ sh shell_parallel_03.sh
2
3
4
5
1
6
7
8
9
10
start_time: 20230129-235234
end_time: 20230129-235236

蚂蚁🐜再小也是肉🥩!


Shell脚本并发控制
http://heibanbai.com.cn/posts/77cc6cb7/
作者
黑伴白
发布于
2023年1月30日
许可协议

“您的支持,我的动力!觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭”

微信二维码

微信支付

支付宝二维码

支付宝支付