如何:在 Bash 中使用并行循环

· Tutorials

如何:在 Bash 中使用并行循环

TL;DR: 使用 & 或 GNU parallel 并行运行 bash 循环,以同时处理多个文件。对于像 FASTQ 处理这样的独立任务,可以减少运行时间。请记住:并行作业数 × 每个作业的内存 = 所需总内存。


问题所在

当任务相互独立时,顺序处理文件会浪费时间:

# 顺序处理:一次处理一个文件
for file in *.fastq.gz; do
    process_sample "$file"  # 等待每个文件处理完成
done
# 总时间:10 个文件 × 30 分钟 = 5 小时

解决方案:并行处理

同时运行多个任务:

# 并行处理:同时处理所有文件
for file in *.fastq.gz; do
    process_sample "$file" &  # & = 在后台运行
done
wait  # 等待所有后台作业完成
# 总时间:30 分钟(如果你有足够的核心/内存)

顺序处理 vs 并行处理


方法一:后台作业 (&)

基本模式

# 定义任务函数
task() {
    input=$1
    output="${input%.fastq.gz}.bam"
    echo "Processing $input"
    bwa mem ref.fa "$input" > "$output"
    echo "Done: $output"
}

# 并行运行
for file in *.fastq.gz; do
    task "$file" &  # 在后台启动
done

wait  # 等待所有作业完成
echo "All files processed"

实际示例:RNA-seq 定量分析

#!/bin/bash

# 配置
data="/path/to/fastqs"
output="/path/to/results"
cores_per_job=4

# 任务函数
quantify_sample() {
    r1=$1
    r2=${r1%_R1.fastq.gz}_R2.fastq.gz
    sample=$(basename ${r1%_R1.fastq.gz})
    
    echo "[$(date)] Starting: $sample"
    
    kallisto quant \
        -i transcripts.idx \
        -o $output/$sample \
        -t $cores_per_job \
        $r1 $r2
    
    echo "[$(date)] Finished: $sample"
}

# 为子 shell 导出函数
export -f quantify_sample
export data output cores_per_job

# 并行运行所有样本
for r1 in $data/*_R1.fastq.gz; do
    quantify_sample "$r1" &
done

wait
echo "All samples quantified"

限制并发作业数

防止系统过载:

max_jobs=8
running_jobs=0

for file in *.fastq.gz; do
    # 如果达到最大作业数,则等待
    while [ $(jobs -r | wc -l) -ge $max_jobs ]; do
        sleep 1
    done
    
    process_sample "$file" &
done

wait

方法二:GNU Parallel(推荐)

GNU parallel 提供更好的控制和监控。

安装

# Ubuntu/Debian
sudo apt install parallel

# macOS
brew install parallel

# conda
conda install -c conda-forge parallel

基本用法

# 使用自动作业管理处理文件
parallel process_sample ::: *.fastq.gz

# 控制并发作业数
parallel -j 4 process_sample ::: *.fastq.gz

# 配对 R1/R2 文件
parallel -j 8 \
    'kallisto quant -i idx -o {1/.} {1} {2}' \
    ::: *_R1.fastq.gz \
    :::+ *_R2.fastq.gz

高级功能:进度和日志记录

# 显示进度条
parallel --bar -j 8 process_sample ::: *.fastq.gz

# 记录每个作业的输出
parallel -j 8 --joblog run.log --results logs/ \
    process_sample ::: *.fastq.gz

# 恢复失败的作业
parallel --resume --joblog run.log -j 8 \
    process_sample ::: *.fastq.gz

双端 FASTQ 示例

#!/bin/bash

# 处理双端测序读数的函数
process_pair() {
    r1=$1
    sample=$(basename ${r1%_R1.fastq.gz})
    r2=${r1%_R1.fastq.gz}_R2.fastq.gz
    
    mixcr analyze rna-seq \
        --species mmu \
        --threads 4 \
        $r1 $r2 \
        output/$sample
}

export -f process_pair

# 使用 GNU parallel 运行(8 个样本 × 4 线程 = 32 核心)
parallel -j 8 process_pair ::: data/*_R1.fastq.gz

资源管理

计算所需资源

场景 计算方式
核心数 并行作业数 × 每个作业的核心数
内存 并行作业数 × 每个作业的内存
时间 总文件数 / 并行作业数 × 每个文件处理时间

示例:SLURM 集群

#!/bin/bash
#SBATCH --job-name=parallel_mixcr
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=32      # 8 个作业 × 4 核心
#SBATCH --mem=300G              # 8 个作业 × ~35G
#SBATCH --time=06:00:00

# 经验法则:每 10GB 内存对应 1 个核心
# 300G / 10 = 30 核心(请求了 32 个,很好)

parallel -j 8 process_sample ::: *.fastq.gz

内存密集型任务

# 基于内存而非核心数限制作业数
# 如果每个作业需要 50GB,而你总共有 256GB:
max_mem_jobs=$((256 / 50))  # = 5 个作业

parallel -j $max_mem_jobs memory_intensive_task ::: files

对比:& vs GNU Parallel

特性 后台 & GNU Parallel
设置 内置 需要安装
作业控制 手动 自动
进度 无 --bar, --eta
日志记录 手动重定向 --joblog
恢复 不支持 --resume
复杂度 简单 功能丰富
最佳适用场景 快速脚本 生产工作流

最佳实践

1. 首先在子集上测试

# 在完整运行前用 2 个样本测试
parallel -j 2 process_sample ::: $(ls *.fastq.gz | head -2)

2. 监控资源使用情况

# 在另一个终端中
watch -n 5 'ps aux | grep process_sample | wc -l'
htop  # 交互式进程查看器

3. 优雅地处理错误

process_with_error_handling() {
    input=$1
    output="${input%.fastq.gz}.bam"
    
    if bwa mem ref.fa "$input" > "$output" 2>&1; then
        echo "SUCCESS: $input"
    else
        echo "FAILED: $input" >> failed.log
        return 1
    fi
}

parallel -j 8 --halt soon,fail=3 \  # 如果 3 个作业失败则停止
    process_with_error_handling ::: *.fastq.gz

4. 组织输出

# 自动创建输出目录
parallel -j 8 \
    'mkdir -p output/{/.} && process {/} output/{/.}' \
    ::: data/*.fastq.gz

常见用例

压缩多个文件

parallel -j 4 gzip ::: *.fastq

下载多个 URL

parallel -j 10 wget ::: $(cat urls.txt)

转换文件格式

parallel -j 8 \
    'samtools view -bS {}.sam > {}.bam' \
    ::: sample1 sample2 sample3

运行参数扫描

# 测试多个参数组合
parallel -j 16 \
    'run_analysis --param1 {1} --param2 {2} > {1}_{2}.out' \
    ::: 0.1 0.5 1.0 \
    ::: 10 50 100

故障排除

"打开文件过多"

原因: 并行作业数超过文件描述符限制
修复:

ulimit -n 4096  # 提高限制

作业静默失败

原因: 没有错误检查
修复: 使用 set -e 并检查退出码:

task() {
    set -e  # 出错时退出
    command || { echo "FAILED: $1"; return 1; }
}

内存溢出

症状: 系统无响应,OOM killer 被激活
修复: 减少 -j 参数或在集群上增加内存请求


性能提示

  1. I/O 密集型任务: 使用比核心数更多的并行作业(例如,在 16 核心上使用 -j 32)
  2. CPU 密集型任务: 使用 并行作业数 = 核心数 / 每个任务的线程数
  3. 混合工作负载: 先启动长作业,后启动短作业:
    # 按文件大小排序(最大的优先)
    ls -S *.fastq.gz | parallel -j 8 process_sample
    

另请参阅:

Parallel Processing