如何:在 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 分钟(如果你有足够的核心/内存)

方法一:后台作业 (&)
基本模式
# 定义任务函数
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 参数或在集群上增加内存请求
性能提示
- I/O 密集型任务: 使用比核心数更多的并行作业(例如,在 16 核心上使用
-j 32) - CPU 密集型任务: 使用
并行作业数 = 核心数 / 每个任务的线程数 - 混合工作负载: 先启动长作业,后启动短作业:
# 按文件大小排序(最大的优先) ls -S *.fastq.gz | parallel -j 8 process_sample
另请参阅: