实现Shell"多线程"
前言
Shell一行行地执行实在是太影响效率.本文将带你提升你的Shell执行效率,采用伪”多线程”的方式启动多个后端进程,最大程度利用cpu性能.
本文代码关键点有一定的注释,方便读者理解并灵活使用.本文示例及部分内容摘录自互联网,由本人整理成以下内容.遵守CC BY-NC-SA 4.0协议.
先问你一个问题.
多线程有什么用?
这里引用知乎pansz用户的回答
- 单进程单线程:一个人在一个桌子上吃菜
- 单进程多线程:多个人在同一个桌子上一起吃菜
- 多进程单线程:多个人每个人在自己的桌子上吃菜
多线程的问题是多个人同时吃一道菜的时候容易发生争抢
例如两个人同时夹一个菜,一个人刚伸出筷子,结果伸到的时候已经被夹走菜了…
此时就必须等一个人夹一口之后,在还给另外一个人夹菜,也就是说资源共享就会发生冲突争抢。
对于 Windows 系统来说,【开桌子】的开销很大,因此 Windows 鼓励大家在一个桌子上吃菜。因此 Windows 多线程学习重点是要大量面对资源争抢与同步方面的问题。
对于 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 # 新建一个fifo类型的文件 |
管道具有存一个读一个,读完一个就少一个,没有则阻塞,放回的可以重复取的特点。这正是队列特性,但问题是如果往管道文件里面放入一段内容,没人取则会阻塞,这样你永远也没办法往管道里面同时放入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 |
在macOS上使用命令 ls /dev/fd/
利用重定向>&
可以为一个FD赋值,使其指向一个非null的文件,其实就是打开一个FD:
1 | 6>&1 |
将FD6指针置为空值null,可关闭FD6:
1 | 6>&- |
一个重定向只在当前命令中有效。通过exec可以使IO重定向在当前shell中长期有效:
1 | # 打开FD6 |
再回到我们刚才的1000位美女要去厕所,解决一个管道文件不能放10把“钥匙”的问题:
先利用exec 6<>/tmp/fd1 创建文件描述符6关联管道文件。
这时,6这个文件描述符就拥有了管道的所有特性(存一个读一个,读完一个就少一个,没有则阻塞,放回的可以重复取)。除此之外,它还拥有一个管道不具有的特性:无限存不阻塞,无限取不阻塞,且不用关心管道内是否为空、是否有内容写入引用文件描述符。
&6可以执行n次echo >&6 往管道里放入n把钥匙。
接下来就是怎么使用FIFO和FD实现shell”多线程”了~
shell”多线程”示例脚本
需求:并发处理1000个命令,如何用shell实现?
方案1.挨个挨个处理
这个是最容易想到的,用for循环1000次即可。
1 | #!/bin/bash |
一个for循环1000次相当于需要处理1000个文件,循环体用sleep 1代表运行一条命令需要的时间,用success$i来标示每条任务。
这样写的问题是,1000条命令都是顺序执行的,假如每条命令的运行时间是1秒,那么1000条命令的运行时间则为1000秒,效率相当低,不满足我们并发处理1000个命令的需求。而且,假如在顺序执行到第900个文件时,发现该文件有问题,那么到这时所需要的时间就是900s!
所以,问题的关键点是:如何实现并发?
方案2.使用’&’+wait 实现“多线程”
我们通过后台运行(&),wait等待所有子后台进程结束实现”多线程”。
1 | #!/bin/bash |
shell中实现并发,就是把循环体的命令用&符号放入后台运行,1000个任务就会并发1000个线程,运行时间2s左右,比起方案一的1000s,已经非常快了。
但问题是,’&’+wait 这种方法对线程并发数不可控。如果有很多文件,系统会随着高并发压力的不断攀升,处理速度变得越来越慢。
打个简单的比方,方案1是有1000块砖,你每次搬一块,虽然慢但是搬得动;方案2是有1000块砖,一次搬1000块,搬到后面是不是会越来越吃力?而下面要说的方案3,则是设置每次搬的数量,比如5块,提高效率又不会伤身体。
方案3.使用FIFO实现“多进程”
先新建一个FIFO,写入一些字符。一个进程开始前会先从这个FIFO中读走一个字符,执行完之后再写回一个字符。如果FIFO中没有字符,该线程就会等待,fifo就成了一个锁。
下面是设置5个线程的例子:
1 | #!/bin/bash |
注意:实际运用中,要先测试代码是否正确,然后再进行多线程。
参考:
本来想写点 xargs 的,但unix上的命令和linux参数不一样,然后感觉精力有限,就不写了.欢迎读者自己去了解Linux xargs命令详解shell高效处理文本(1):xargs并行处理