惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
有赞技术团队
有赞技术团队
J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
美团技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Hugging Face - Blog
Hugging Face - Blog
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
Check Point Blog
博客园 - 【当耐特】
The GitHub Blog
The GitHub Blog
Recent Announcements
Recent Announcements
The Cloudflare Blog
Microsoft Azure Blog
Microsoft Azure Blog
腾讯CDC
Vercel News
Vercel News
IT之家
IT之家
MyScale Blog
MyScale Blog
博客园_首页
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
罗磊的独立博客

LanYunのBlog

优化macOS Dock栏的显示与隐藏 解决macOS全屏模式下某些情况显示原始鼠标指针的问题 Yarn从Classic迁移到Modern nginx自编译 用santa对二进制进行监控和授权 ESXI 8.0安装黑群晖DSM 7.x openwrt编译过程记录 编译monero For macOS ARM 自建busuanzi不蒜子平台统计平台 浅记博客更新日志 (机翻)Kubo配置文件 修改brew services中服务的启动参数 技术杂记📝 解决Surge模块不能同步问题 为Arch Linux配置邮件服务 自编译NutClient-ESXI,更改邮件逻辑 解决ESXI和群晖使用SSH密钥登录问题 为J4125软路由ESXI中群晖提供UPS服务 在arch linux上搭建rustdesk server 解决虚拟机扩容中GPT PMBR大小不符问题 搭建Tuic V5 优雅地为macOS提供python多版本的支持 修复root路径下文件权限问题 解决macOS 14上利用Whisky运行steam时有关问题 在macOS中用PhpStorm配置Xdebug调试 利用CloudFlare Workers快速自建一个IP获取器 如何自动续期证书? 从0开始速配VPS用于网站等 搭建一个基于ChatGPT的QQ机器人(Yunzai-Bot) For Mac 010 Editor For Mac 13.0.1 Crack 破解
实现Shell“多线程”
LanYun · 2022-05-05 · via LanYunのBlog

前言

Shell一行行地执行实在是太影响效率.本文将带你提升你的Shell执行效率,采用伪”多线程”的方式启动多个后端进程,最大程度利用cpu性能.

本文代码关键点有一定的注释,方便读者理解并灵活使用.本文示例及部分内容摘录自互联网,由本人整理成以下内容.遵守CC BY-NC-SA 4.0协议.


先问你一个问题.

多线程有什么用?

这里引用知乎pansz用户的回答

  1. 单进程单线程:一个人在一个桌子上吃菜
  2. 单进程多线程:多个人在同一个桌子上一起吃菜
  3. 多进程单线程:多个人每个人在自己的桌子上吃菜

多线程的问题是多个人同时吃一道菜的时候容易发生争抢
例如两个人同时夹一个菜,一个人刚伸出筷子,结果伸到的时候已经被夹走菜了…
此时就必须等一个人夹一口之后,在还给另外一个人夹菜,也就是说资源共享就会发生冲突争抢。

  1. 对于 Windows 系统来说,【开桌子】的开销很大,因此 Windows 鼓励大家在一个桌子上吃菜。因此 Windows 多线程学习重点是要大量面对资源争抢与同步方面的问题。

  2. 对于 Linux 系统来说,【开桌子】的开销很小,因此 Linux 鼓励大家尽量每个人都开自己的桌子吃菜。这带来新的问题是:坐在两张不同的桌子上,说话不方便。因此,Linux 下的学习重点大家要学习进程间通讯的方法。

开桌子的意思是指创建进程,开销这里主要指的是时间开销.

注意,线程和进程是不同的!

多线程就像火车的多个车厢,而进程则是火车。

在我们实现多线程之前,先了解2个概念.
有名管道FIFO和File Descriptor (FD)

有名管道FIFO

管道文件有两种,一个是有名管道,一个是匿名管道。

“FIFO”则是有名管道(有名字的)。
它的特性是:如果一个进程打开FIFO文件进行写操作,而另一个进程对之进行读操作,数据就可以如同在shell或者其它地方常见的的匿名管道一样流线执行。

利用有名管道FIFO的上述特性就可以实现一个队列控制了。

**你可以这样想:一个女士公共厕所总共就10个蹲位,这个蹲位就是队列长度。**女厕所门口放着10把钥匙,要想上厕所必须拿一把钥匙,上完厕所后归还钥匙,下一个人就可以拿钥匙进去上厕所了。好,现在同时来了1000位美女要上厕所,那前10个人抢到钥匙进去上厕所了,后面的990人就需要等一个人出来归还钥匙后才可以拿到钥匙进去上厕所,这样10把钥匙就实现了控制1000人上厕所的任务(os中称之为信号量)。

mkfifo命令可用于创建fifo:

1
mkfifo $tmp_fifofile      

管道具有存一个读一个,读完一个就少一个,没有则阻塞,放回的可以重复取的特点。这正是队列特性,但问题是如果往管道文件里面放入一段内容,没人取则会阻塞,这样你永远也没办法往管道里面同时放入10段内容(相当于10把钥匙),解决这个问题的关键就是文件描述符(File Descriptor,FD)了。

File Descriptor (FD)

Linux shell中的File Descriptor (FD),可以理解为一个指向文件的指针。

默认有三个FD:0,1,2。Shell中还允许有3到9的FD,默认都没有打开,可以认为指向null。使用如下命令可查看FD:

1
ls /proc/self/fd

image-20220511132755462

在macOS上使用命令 ls /dev/fd/

image-20220511132955394

利用重定向>&可以为一个FD赋值,使其指向一个非null的文件,其实就是打开一个FD:

1
2
3
6>&1
# 可以理解为将FD6指针指向FD1指针指向的文件
# 这样,FD6和FD1就同时指向同一个文件

将FD6指针置为空值null,可关闭FD6:

1
6>&-

一个重定向只在当前命令中有效。通过exec可以使IO重定向在当前shell中长期有效:

1
2
3
4
# 打开FD6
exec 6>&1
# 关闭FD6
exec 6>&-

再回到我们刚才的1000位美女要去厕所,解决一个管道文件不能放10把“钥匙”的问题:

先利用exec 6<>/tmp/fd1 创建文件描述符6关联管道文件。
这时,6这个文件描述符就拥有了管道的所有特性(存一个读一个,读完一个就少一个,没有则阻塞,放回的可以重复取)。除此之外,它还拥有一个管道不具有的特性:无限存不阻塞,无限取不阻塞,且不用关心管道内是否为空、是否有内容写入引用文件描述符。
&6可以执行n次echo >&6 往管道里放入n把钥匙。

接下来就是怎么使用FIFO和FD实现shell”多线程”了~

**需求:**并发处理1000个命令,如何用shell实现?

方案1.挨个挨个处理

这个是最容易想到的,用for循环1000次即可。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

date # 脚本开始时间

for ((i=1;i<=1000;i++))
do
sleep 1 #sleep 1用来模仿执行一条命令需要花费的时间(可以用真实命令来代替)
echo 'success'$i;
done

date # 脚本结束时间

一个for循环1000次相当于需要处理1000个文件,循环体用sleep 1代表运行一条命令需要的时间,用success$i来标示每条任务。

这样写的问题是,1000条命令都是顺序执行的,假如每条命令的运行时间是1秒,那么1000条命令的运行时间则为1000秒,效率相当低,不满足我们并发处理1000个命令的需求。而且,假如在顺序执行到第900个文件时,发现该文件有问题,那么到这时所需要的时间就是900s!

所以,问题的关键点是:如何实现并发?

方案2.使用’&’+wait 实现“多线程”

我们通过后台运行(&),wait等待所有子后台进程结束实现”多线程”。

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

date # 脚本开始时间

for ((i=1;i<=1000;i++))
do
{
sleep 1 #sleep 1用以模仿执行一条命令需要花费的时间(可以用真实命令来代替)
echo 'success'$i;
}& #用{}把循环体括起来,后加一个&符号,代表每次循环都把命令放入后台运行
#一旦放入后台,就意味着{}里面的命令交给操作系统的一个线程处理了
#循环了1000次,就有1000个&将任务放入后台,操作系统会并发1000个线程来处理
done
wait #wait命令表示。等待上面的命令(放入后台的任务)都执行完毕了再往下执行

date # 脚本结束时间

shell中实现并发,就是把循环体的命令用&符号放入后台运行,1000个任务就会并发1000个线程,运行时间2s左右,比起方案一的1000s,已经非常快了。

但问题是,’&’+wait 这种方法对线程并发数不可控。如果有很多文件,系统会随着高并发压力的不断攀升,处理速度变得越来越慢。

打个简单的比方,方案1是有1000块砖,你每次搬一块,虽然慢但是搬得动;方案2是有1000块砖,一次搬1000块,搬到后面是不是会越来越吃力?而下面要说的方案3,则是设置每次搬的数量,比如5块,提高效率又不会伤身体。

方案3.使用FIFO实现“多进程”

先新建一个FIFO,写入一些字符。一个进程开始前会先从这个FIFO中读走一个字符,执行完之后再写回一个字符。如果FIFO中没有字符,该线程就会等待,fifo就成了一个锁。

下面是设置5个线程的例子:

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
#!/bin/bash
#将1000个bam文件进行转bed

start_time=`date +%s` #定义脚本运行的开始时间

tmp_fifofile="/tmp/$$.fifo"
mkfifo $tmp_fifofile # 新建一个FIFO类型的文件
exec 6<>$tmp_fifofile # 将FD6指向FIFO类型
rm $tmp_fifofile #删也可以,

thread_num=5 # 定义最大线程数

#根据线程总数量设置令牌个数
#实际上就是在fd6中放置了$thread_num个回车符
for ((i=0;i<${thread_num};i++));do
echo
done >&6

for i in data/*.bam # 找到data文件夹下所有bam格式的文件
do
# 一个read -u6命令执行一次,就从FD6中减去一个回车符,然后向下执行
# 当FD6中没有回车符时,就停止,从而实现线程数量控制
read -u6
{
echo "great" # 可以用实际命令代替
echo >&6 # 当进程结束以后,再向FD6中加上一个回车符,即补上了read -u6减去的那个
} &
done

wait # 要有wait,等待所有线程结束

stop_time=`date +%s` # 定义脚本运行的结束时间
echo "TIME:`expr $stop_time - $start_time`" # 输出脚本运行时间

exec 6>&- # 关闭FD6
echo "over" # 表示脚本运行结束

注意:实际运用中,要先测试代码是否正确,然后再进行多线程。

参考:

1

2

本来想写点 xargs 的,但unix上的命令和linux参数不一样,然后感觉精力有限,就不写了.欢迎读者自己去了解Linux xargs命令详解shell高效处理文本(1):xargs并行处理