相关文章推荐
英姿勃勃的山羊  ·  JQuery Ajax POST/GET ...·  11 月前    · 
性感的绿豆  ·  对 ASP.NET Core ...·  1 年前    · 
飘逸的野马  ·  Stable ...·  1 年前    · 
[Linux 1] Shell“ 多线程”,提高工作效率

[Linux 1] Shell“ 多线程”,提高工作效率

我们都想提高效率,节省时间。

因此,工作中处理数据不能停留在一个一个操作的阶段,要转换到批量操作。

批量操作的一个实现方法,就是多线程。

(注释超详细,读者可举一反三,适当修改模板,效果显著!)


本文主要内容为:

1. 多线程的意义
2. FIFO与File Descriptor
3. shell“多线程”示例脚本

一、多线程有什么用?

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

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

多线程有什么用?pansz.


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

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


二、在实现多线程之前,首先了解2个概念:

1.有名管道FIFO
2.File Descriptor (FD) 

1. 有名管道FIFO

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

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

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

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


多线程执行for循环shell脚本 | dubendi. CSDN, 2017/12/29

mkfifo命令可用于创建fifo:

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


多线程执行for循环shell脚本 | dubendi. CSDN, 2017/12/29


2. File Descriptor (FD)

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

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

ls /proc/self/fd

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

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

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

6>&-

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

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

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

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

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


三、shell“多线程”示例脚本

shell中没有真正意义上的多线程,但可以通过启动多个后端进程,最大程度利用cpu性能。

旭东的博客


需求: 并发处理1000个bam文件进行转bed,如何用shell实现?


方案1.挨个挨个处理

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

#!/bin/bash
# bam to bed
date # 脚本开始时间
for ((i=1;i<=1000;i++))
        sleep 1  #sleep 1用来模仿执行一条命令需要花费的时间(可以用真实命令来代替)
        echo 'success'$i;       
date # 脚本结束时间

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

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

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


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

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

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

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

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

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


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

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

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

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