实现shell脚本多线程

发布时间 2023-09-11 18:21:35作者: 背对背依靠

默认的情况下,Shell脚本中的命令是串行执行,必须等到前一条命令执行完后才执行接下来的命令,但是如果有一大批命令需要执行,而且互相又没有影响的情况下,那么就要使用并发的方式执行命令了。

因为Shell本身并不提供多线程机制,因此需要借助其他技术(如有名管道)来实现多线程的通信。

后台执行实现并发

将指定的命令或程序通过&符号放入后台执行,由操作系统来管理和调度。这样该程序就不会占用当前bash,其他命令也不用等待前面命令执行完再继续,而且可以放入多个任务到后台,这样就实现了多任务并发。

但是这样有一个问题就是没法控制线程的数量,例如放入1000个程序到后台执行,操作系统会同时开启1000个进程开进行执行,那么系统超出负载后,会有性能变差或者宕机风险。

后台执行实现并发操作方法:
方法一:大括号
使用花括号 { } 将一组命令包围起来时,这些命令会作为一个整体在当前 shell 中顺序执行。然后,通过在花括号后面添加 & 符号,将整个命令组放到后台运行
{
command
}&
方法二:小括号
使用小括号 ( ) 将一组命令包围起来时,这些命令会在一个子shell中执行。子shell是由当前 shell 创建的一个独立的执行环境。通过在小括号后面添加 & 符号,你将这个子shell放到后台运行。这意味着子shell的执行将与当前 shell 并行进行,由操作系统来管理和调度。
(
command;
)&

例如

#!/bin/bash
echo "当前进程PID: $$"
for (( i=0; i<10; i++ ))
do
    {
        ping www.baidu.com &> /dev/null
        sleep 100
    }&
done

该shell脚本将{ }&中的内容作为了一个整体放入后台执行,放入后台后由操作系统进行托管,有几次循环操作系统就会开启多少个shell进程来运行该程序;

通过管道控制并发
特点:借用管道的特性来实现对shell后台进程数量的控制

例如
该脚本实现将src路径下的视频文件迁移到dest路径下,因为数据量大且为了充分利用计算机的性能,可以采用shell多线程的方式批量执行迁移命令。

如果直接全部将相关程序直接后台运行,交给操作系统托管,例如有十万个视频文件,那么操作系统就会开始十万个进程来执行相关程序,这样很容易就会把计算机给搞崩溃。

所以采用管道结合文件描述符的方式来控制每次开启多少个子进程来执行迁移命令,从而实现了并发的控制。

#!/bin/bash
# 源路径
src=/home/ehigh/work/html/EHCommon/resources/camera/video  
# 目的路径
dest=/home/ehigh/Videos   
# 线程数
thread_task=8    
log=/home/ehigh/videotransfer.log
transfer_record=/home/ehigh/transfer_record.txt

[ ! -d "$src" ] && { echo "$src Directory does not exist" && exit 1; }
[ ! -d "$dest" ] && { echo "$dest Directory does not exist" && exit 1; }
[ ! -f "$transfer_record" ] && touch $transfer_record

mkfifo transferfifo
exec 1086<>transferfifo
rm -rf transferfifo

# 写入占位符
for ((n=1; n<=$thread_task; n++))
do
    echo >&1086
done

videofile=$(ls $src/*.mp4)
for file in $videofile
do
  # 读取管道中的内容
    read -u1086
	# 作为一个整体,后台运行
    {
        if ! grep $file $transfer_record >>/dev/null; then
            cp -p $file $dest
            if [[ $? -eq 0 ]]; then
                echo $file >> $transfer_record
            else
                echo "$(date --rfc-3339=seconds) $file transfer failure." | tee -a $log
                exit 1
            fi
        fi
        echo >&1086
    }&
done
# 等待所有子程序都执行完毕后再进行下一步操作
wait

echo "$(date --rfc-3339=seconds) File transfer completed." | tee -a $log

使用mkfifo创建命名管道后,将文件描述符1086和该管道文件进行关联,关联后1086这个文件描述符就具有了管道的特性,且文件描述符还有个特性就是无限存不阻塞,无限取不阻塞,且不用关心管道内是否为空、是否有内容写入引用文件描述符;

文件描述符和命名管道关联成功后,删除这个命名管道文件不会影响文件描述符 1086 与管道之间的关联和功能,因为文件描述符已经被成功地指向了管道。所以,一旦文件描述符与管道关联成功,我们就可以安全地删除管道文件而不会影响后续对文件描述符的操作。因为transferfifo 是一个命名管道文件,而管道是操作系统提供的一种特性,用于实现进程间的通信。在这种情况下,通过将文件描述符与命名管道文件进行关联,实际上是将文件描述符与操作系统提供的管道特性进行关联。

通过往命名管道(不是命名管道文件,因为命名管道文件已经被我们删除了,文件描述符 1086 关联的命名管道是由操作系统分配和管理的一个不可见的实体)批量写入占位符,因为我们需要同时运行的线程数是8,所以就往命名管道内一次性写入8个占位符(echo命令输出的就是空行)

每次循环迭代时,read命会往管道里面读取一个占位符,等到8个占位符都读取完了后,就会进入阻塞状态。因为这是管道的一个特性,管道里面没数据了,再读取的时候就会进入阻塞状态,此时就阻塞在了(read -u1086)这儿,后面的程序不会执行了。因为循环里面的代码块是通过 {} 和 & 符号将代码块包裹起来,使其成为一个后台进程,由操作系统来进行托管,这就是有8个代码块程序会开启八个线程的原因。

因为每个后台进程里面都通过( echo >&1086 命令)将一个占位符写回与文件描述符 1086 关联的命名管道中。因为有 8 个后台进程,所以在这 8 个后台进程都执行完毕后,又会写入 8 个占位符,使得占位符的数量恢复到 8 个。这样,当所有占位符都被取走时,新的线程会被阻塞,说明后台正有8个进程再运行。等带八个进程运行完成后,每个进程又会通过 echo >&1086 写入一个占位符到命名管道中,所以最后占位符又恢复到了8个,这样就又可以继续向下执行。

所以通过控制命名管道占位符数量的方式来实现线程数量的控制。