如何在 shell 中实现并行执行

如何在 shell 中实现并行执行

今天,有个同事问我,他的 shell 中要执行一个迭代200次的循环,因为每一次循环都需要消耗一定的时间,执行起来比较慢,问我可不可以改成并行执行,循环一次执行10个任务,循环20次来完成所有的任务。

什么是并行?并行,是一种常见的任务执行过程模式,指可以同时执行两个或多个程序,与之相对的则是串行。还应该注意,并行不是并发,两者之间是有明显区别的,有些开发者经常搞混。并发是指服务系统支持两个或多个任务同时存在,同时存在并不意味着同时执行,因为服务系统单位时间上只执行一个任务,其它的任务以等待的形式存在。

下面就同事的问题,介绍如何在 shell 中解决并行控制的方法。

串行改为并行

首先,先来看一个串行的例子:

>  for i in `seq 1 10`
do
    sleep 1; echo $i
done

这是一个迭代次数为10的循环,每一个循环都会等待 1 秒,执行总时长约等于 10 秒。sleep 1 会阻塞循环,只有 sleep 1 执行结果,才会进入下一循环,这是典型的串行模式。

shell 提供了一种把命令提交到后台任务队列的机制,即使用 命令 & 将命令控制权交到后台并立即返回执行下个任务。

>  for i in `seq 1 10`
do
    sleep 1 &; echo $i
done

还是这个例子,多了个 & 符,其作用是将命令 sleep 1 提交到后台去执行,而 for 无须等待就可进入下一次循环。所以上面的 for 循环在 1 秒未到的时间内就执行完毕,然后系统会逐个执行 sleep 1 并向终端报告命令执行结束。

并行-等待模式

上面将串行循环改为并行循环的例子,并没考虑这样的情况。

>  for i in `seq 1 10`
do
    sleep 1 &; echo $i
done
echo "all weakup"

这个例子要求在 for 循环中的所有命令(sleep 1)都执行完之后,打印 “all weakup”。如果按照这段脚本,发现情况并不是这样的,因为 for 循环不会等待 sleep 命令执行结束后才结束,而是把命令提交给系统后自己就退出了,进而还没有1个 sleep 执行完毕之前,“all weakup” 就已经打印了。

为了达到题目要求,需要在 echo "all weakup" 命令之前,加上 wait命令,意为等待上面所有 & 作用过的后台任务执行结束后才继续往下。

>  for i in `seq 1 10`
do
    sleep 1 &; echo $i
done
wait
echo "all weakup"

并行度控制

下面列举的方案并不包括所有可能的实现方案。

方案1-控制一次性提交的后台任务数量

上面的示例中,for 循环会将所有命令转为后台执行。显然,如果每个命令需要比较大的开销,并且循环次数太多,这个方法并不可取。那么,要求 for 循环有部分执行或循环达到一定次数后就要 wait,等待前一批次的所提交的任务执行完之后,再提交一定数量命令(再循环一定次数)后再继续 wait。虽然这种方案并不优雅,但至少不会导致一次性向系统提交过多后台任务。

看看下面的例子:

degree=4
for i in `seq 1 10`
do
    sleep 1 & # 提交到后台的任务
    echo $i
    [ `expr $i % $degree` -eq 0 ] && wait
done

上面示例,设置了一个变量 degree,用来表示并行度,在整个循环中控制阻塞的关键就是于语句 [ ``expr $i % $degree`` -eq 0 ] && wait,即在第 n 次循环时,如果 n 恰好对 degree 求模等于0时,那么循环先阻塞,等待前面 n 个后台任务执行完毕后再继续,以此类推。

方案2-利用队列来控制提交的任务数量

并行度控制,原理还不算复杂,但因为 shell 的原生数据结构支持较弱,使用 shell 来实现并行度控制就比较麻烦。

与 c、java、python 等语言实现并行度的原理基本一致,都是设置一个类似线程池或者工作池的数组,

方案3-利用命名管道来做任务队列

大致原理是创建一个 FIFO 命名管道来做为队列,先放进一定量的字符到这个管道做为信号。然后在一个 for 循环中,每循环一次,从管道中读取一个字符信号,提交一个后台任务,并往这个管道中追加一个字符信号,保持管道中的字符信号数量。

是不是很像当 java 中的线程池、golang 中的 chan。没有接触过命名管理、文件描述符等概念的,可能会比较难以理解下面示例中的部分细节。

_fifofile="$$.fifo"
mkfifo $_fifofile     # 创建一个FIFO类型的文件
exec 6<>$_fifofile    # 将文件描述符6写入 FIFO 管道, 这里6也可以是其它数字
rm $_fifofile         # 删也可以,

degree=4  # 定义并行度

#根据并行度设置信号个数
#事实上是在fd6中放置了$degree个回车符
for ((i=0;i<${degree};i++));do
    echo
done >&6

for i in `seq 1 20` # 循环20次
do
    # 从管道中读取(消费掉)一个字符信号
    # 当FD6中没有回车符时,停止,实现并行度控制
    read -u6
    {
        sleep 1  # 实际任务的命令
        echo $i
        echo >&6 # 当进程结束以后,再向管道追加一个信号,保持管道中的信号总数量
    } &
done

wait # 等待所有任务结束

exec 6>&- # 关闭管道

方案1-控制一次性提交的后台任务数量 是采用分批次提交的思路,

方案2 则是利用消费命名管道时read 会出现等待的特性,因为是每执行完一个任务后就追加一个信号,等同释放一个任务执行名额,总是保持任务并行执行数为 degree。

因为采用队列模型,当执行完一个任务,马上就会有另一个任务被启动,在服务系统设计时,这种模式的任务控制是系统资源利用率最高的。

shell 脚本中$ , , ,#,$?含义
$0 这个程式的执行名字
$n 这个程式的第n个参数值,n=1…9
$* 这个程式的所有参数,此选项参数可超过9个。
$# 这个程式的参数个数
$$ 这个程式的PID(脚本运行的当前进程ID号)
$! 执行上一个背景指令的PID(后台运行的最后一个进程的进程ID号)
$? 执行上一个指令的返回值 (显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误)
$- 显示shell使用的当前选项,与set命令功能相同
@ 跟 @ 跟@跟*类似,但是可以当作数组用
命名管道
命名管道处理的思路:

就相当于此时有10个开水房间,配有10把钥匙,此时有100个人要打开水,那么前10个人抢到钥匙,就先进去打开水,后面的90个人就需要等前面一个人出来后还回钥匙,在拿着钥匙进去打开水,这样就能控制100个人打开水的任务,同时不会将系统的资源一次性耗用太多,增加压力,减小处理速度。

知识点:

1、命名管道的特性

如果管道内容为空,则阻塞
管道具有读一个少一个,存一个读一个的性质,放回去的可以重复取
可以实现队列控制

2、如果管道放一段内容没有人取,则会阻塞

解决上述问题,可以通过文件描述符
文件描述符具有管道的所有特性,同时还具有一个管道不具有的特性:无限存不阻塞,无限取不阻塞,无需关注管道内容

命名管道创建方式
# 1、创建命名管道
mkfifo /tmp/fl
#2、创建文件描述符100,并关联到管道文件
exec 100<>/tmp/fl
#3、调用文件描述符,向管道里存放内容,同时也表示将用完的管道内容在放回管道
echo >&100
#4、读取文件描述符关联管道中的内容
`read -u100``
#5、关闭文件描述符的读和写
exec 100<&-
exec 100>&-

方案4-使用 xargs 命令的控制参数

我是在写本文的时候,才发现原来 xargs 有一个控制并行的参数。

> seq 20 | xargs -I % -P4 sh -c 'echo %; sleep 1s'

关键在于 xargs-P 参数,指一次性接收多少个参数,默认为1。使用 -I % 指定在命令中可以使用 % 符来表示接收到的参数

方案5-使用 parallel 命令行工具

gnu linux 的生态中,有一个专门用来处理本文所述并行控制场景的工具,名字叫 parallel

官方链接

下面还是使用打印序列号的例子来演示如果控制并行:

> seq 20 | parallel -j 4 "echo {}; sleep 1"

命令格式与 xargs 类似,使用 -j 来指定并行度;使用 {} 来表示参数。

<think>嗯,用户想知道如何用Shell脚本并行执行多个'make run'命令。这个问题应该涉及到多进程或者多线程的处理吧。我之前学过一些Shell脚本的基础知识,记得可以通过后台运行命令和等待完成来实现。 首先,用户的需求是同时运行多个make run,而不是依次执行。那在Shell中,每个命令后面加上&符号可以让它在后台运行。比如make run &,这样每个命令都会在后台启动,然后脚本继续执行下一条命令,这样就能并行了。 不过,这样可能会一下子启动太多进程,导致系统资源不足。可能需要控制并发数量。比如,用户可能希望同时运行4个进程,而不是全部同时启动。这时候可以用一些方法,比如使用计数器或者进程替换来控制。 另外,用户提到“多线程或多进程”。在Shell中,多进程更常见,因为每个后台命令都是一个独立的进程。而多线程的话,可能需要使用像xargs这样的工具,或者GNU parallel这样的第三方工具。不过用户可能希望用内置的命令解决,所以xargs的-P选项可能更合适。 比如,使用seq生成一定数量的任务,然后通过xargs -P来指定并行数。或者用循环,结合计数器,每次启动一定数量的后台任务,然后等待它们完成后再继续下一批。 还有一个问题是,用户可能需要在所有任务完成后进行后续处理,所以需要wait命令来等待所有后台进程结束。比如,在循环中启动所有任务后,执行wait,这样脚本会等待所有子进程结束再继续。 示例代码的话,最简单的可能是用for循环加上&和wait。比如: for i in {1..4}; do make run & done wait 这样会同时启动4个make run,然后等待它们全部完成。但这样无法控制并发数量,如果任务很多的话,一下子启动太多可能有问题。这时候可以考虑用更复杂的控制,比如每次只允许N个进程同时运行,使用计数器或者命名管道(mkfifo)来管理。 或者使用xargs,比如: echo {1..10} | xargs -n1 -P4 -I{} make run 这里-P4指定同时运行4个进程。不过需要先生成参数列表,可能用户的任务不需要参数,但可以这样处理。 另外,用户可能希望脚本兼容不同的系统,所以需要考虑是否安装了xargs或者GNU parallel。如果用户的环境支持,使用GNU parallel会更方便,但可能需要额外安装。 总结下来,可能的解决方案有: 1. 简单后台运行加wait:适用于任务数量少,资源足够的情况。 2. 使用xargs的-P参数:方便控制并发数,但需要参数输入。 3. 使用计数器在循环中控制并发数:比如每次启动N个任务,然后等待一批完成后再启动下一批。 4. 使用GNU parallel工具:更强大,但需要安装。 然后,根据用户的示例,可能需要提供一个基础的脚本,使用后台运行和wait,以及更高级的控制并发数的例子。比如,使用一个计数器变量,在循环中检查当前后台进程的数量,不超过设定的最大值。 例如: max_jobs=4 for i in {1..10}; do while (( $(jobs -p | wc -l) >= max_jobs )); do sleep 1 done make run & done wait 不过这种方法可能在不同的Shell中有不同的表现,可能需要使用jobs -r来查看正在运行的作业数。 或者,使用文件描述符和mkfifo来控制,比如: mkfifo tmp exec 3<>tmp rm tmp for ((i=0; i<4; i++)); do echo >&3 done for i in {1..10}; do read -u3 { make run echo >&3 } & done wait 这种方法利用命名管道来限制并发数,每次从管道读取一个令牌,执行任务后放回令牌,这样可以保证最多有4个进程同时运行。 用户可能希望看到不同的方法,所以应该在回答中涵盖几种不同的实现方式,并说明优缺点。同时,要注意代码的正确性和可测试性,确保用户可以直接复制使用。 另外,引用相关的资料,比如站内引用中的[^1]提到可以在循环输出后添加处理命令,可能涉及到管道和重定向,但这里的问题是关于并行执行,所以可能引用[^2]关于rsync多进程同步的方法,或者[^3]中的线程和信号量,不过Shell脚本通常用进程而非线程。可能引用[^4]中的阿姆达尔定律,说明并行化的加速限制,但不确定是否相关,可能不需要。 最后,生成相关问题,比如如何控制并发数量,如何收集输出,如何处理错误,或者使用其他工具如GNU parallel等。</think>### 如何通过Shell脚本实现多进程并行运行多个`make run`命令 #### 方法1:基础并行(无并发控制) 将所有`make run`命令放入后台运行,最后等待完成: ```bash # 启动4个并行任务 for i in {1..4}; do make run & # &符号将命令放入后台执行 done wait # 等待所有后台任务结束 ``` #### 方法2:使用`xargs`控制并发数 通过`xargs -P`指定最大并行进程数: ```bash # 生成任务列表(例如10个任务),每次并行4个 seq 1 10 | xargs -n1 -P4 -I{} make run ``` - `-P4`表示同时运行4个进程[^2] - `seq 1 10`生成10个参数(占位符`{}`未实际使用) #### 方法3:动态控制并发数(高级) 使用计数器限制并发数,避免资源过载: ```bash max_parallel=4 # 最大并发数 count=0 for i in {1..10}; do ((count++ >= max_parallel)) && wait -n && ((count--)) # 等待任意任务完成 make run & # 启动新任务 done wait # 等待剩余任务 ``` #### 方法4:使用命名管道(FIFO) 通过文件描述符精确控制并发流: ```bash mkfifo tmp exec 3<>tmp # 创建文件描述符3关联FIFO rm tmp # 删除临时文件 # 初始化4个"令牌" for ((i=0; i<4; i++)); do echo >&3 done # 启动10个任务 for i in {1..10}; do read -u3 # 获取令牌 { make run echo >&3 # 归还令牌 } & done wait ``` #### 关键说明 1. **后台执行符`&`**:将命令放入子Shell执行,不阻塞主进程 2. **`wait`命令**:确保所有子进程完成后再继续后续操作 3. **输出处理**:若需收集日志,可添加重定向如`make run > log_$i.txt 2>&1`
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值