Shell | 多线程并行计算
多线程并行计算可以提高效率,节省时间。最近工作中有一批数据需要处理,学习了一下批量多线程操作。
多线程
线程与进程是不同的,线程相当于火车车厢,进程相当于火车。
打个比方:
- 单进程单线程:一个人在一张桌子上吃饭
- 单进程多线程:多个人在一张桌子上吃饭
- 多进程多线程:多个人在多张桌子上吃饭
管道
管道文件有两种:有名管道,匿名管道
批量提交脚本会导致I/O严重负荷,我们希望控制提交脚本的数量,并且每次完成一个脚本之后自动提交一个脚本。FIFO有名管道就可以实现这点。它的特性是如果一个进程打开FIFO文件进行写操作,而另一个进程对之进行读操作,数据就可以如同在Shell或者其他地方常见的匿名管道一样流线执行。因此利用有名管道FIFO的上述特性就可以实现一个队列控制。
mkfifo
命令用于创建fifo:
mkfifo $tmp_fifofile #新建一个fifo类型文件
管道具有存一个读一个,读完一个就少一个,没有则阻塞,放回的可以重复取的特点。这正是队列特性,但问题是如果往管道文件里面放入一段内容,没人取则会阻塞,这样你永远也没办法往管道里面同时放入多段内容,解决这个问题的关键就是文件描述符(File Descriptor,FD)了。
文件描述符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个bam文件转化为bed文件,如何用Shell实现。
我们一般的想法就是for循环进行处理
#!/bin/bash
# bam to bed
date # 脚本开始时间
for ((i=1;i<=1000;i++))
do
bam2bed #这里执行自己的脚本
echo " $i finished! "
done
date # 脚本结束时间
这种处理方法需要循环1000次,花费的时间肯定很长,可以考虑并发,一次性提交1000个样本同时处理。可以采用&
+ wait
实现多线程
#!/bin/bash
# bam to bed
date # 脚本开始时间
for ((i=1;i<=1000;i++))
do
{
bam2bed #这里执行自己的脚本
echo " $i finished! "
}& #用{}把循环体括起来,后加一个&符号,代表每次循环都把命令放入后台运行
#一旦放入后台,就意味着{}里面的命令交给操作系统的一个线程处理了
#循环了1000次,就有1000个&将任务放入后台,操作系统会并发1000个线程来处理
done
wait #wait命令表示。等待上面的命令(放入后台的任务)都执行完毕了再往下执行
date # 脚本结束时间
Shell实现并发就是通过&命令符将循环体的命令放入后台运行,但是这种方法对线程并发数不可控,系统也会随着高并发压力的不断攀升,处理速度会变得越来越慢,所以这种方法针对少量的文件可行,但是一旦文件数量大,处理速度是很慢的。
为解决这个问题,就可以使用FIFO实现“多进程”
先新建一个FIFO,写入一些字符。一个进程开始前会先从这个FIFO中读走一个字符,执行完之后再写回一个字符。如果FIFO中没有字符,该线程就会等待,fifo就成了一个锁。
下面是设置32个线程的例子:
#!/bin/bash
# bam to bed
start_time=`date +%s` #定义脚本运行的开始时间
tmp_fifofile="/tmp/$$.fifo"
mkfifo $tmp_fifofile # 新建一个FIFO类型的文件
exec 6<>$tmp_fifofile # 将FD6指向FIFO类型, 这里6也可以是其它数字
rm $tmp_fifofile #删也可以,
thread_num=32 # 定义最大线程数
#根据线程总数量设置令牌个数
#事实上就是在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
{
bam2bed # 可以用实际命令代替
echo >&6 # 当进程结束以后,再向FD6中加上一个回车符,即补上了read -u6减去的那个
} &
done
wait # 要有wait,等待所有线程结束
stop_time=`date +%s` # 定义脚本运行的结束时间
echo "TIME:`expr $stop_time - $start_time`" # 输出脚本运行时间
exec 6>&- # 关闭FD6,最后一定要记得关闭FIFO
echo "over" # 表示脚本运行结束