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" # 表示脚本运行结束

参考:

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

Researcher

I am a PhD student of Crop Genetics and Breeding at the Zhejiang University Crop Science Lab. My research interests covers a range of issues:Population Genetics Evolution and Ecotype Divergence Analysis of Oilseed Rape, Genome-wide Association Study (GWAS) of Agronomic Traits.

comments powered by Disqus