跳转到内容

GNU Parallel

GNU Parallel 是一个用于并行执行 Shell 命令的工具,能够利用 CPU 的多核特性,将原本需要串行执行的任务并行化处理,从而极大地提高执行效率。特别适用于批量文件处理、大规模数据转换、网络请求以及计算密集型任务。

Terminal window
parallel [选项] "command {}" ::: arg1 arg2 ...
# 或者通过管道
cat args.txt | parallel [选项] "command {}"
  • 选项:控制并发数、日志、远程执行等行为
  • command:要运行的命令模板
  • ::::分隔符,后面紧跟参数列表(类似于标准输入)
  • {}:占位符,会被替换为具体的参数

如果命令中没有包含 {},Parallel 默认会在命令末尾追加参数,建议显式写出 {} 以增加可读性。

Parallel 的核心在于任务调度与输出缓冲,它不是简单地后台运行命令,而是作为一个作业管理器存在。

Parallel 的工作过程可以分为三个阶段:

step1 输入解析与分发

  • 读取标准输入(Stdin)或命令行参数(:::)
  • 根据指定的并发数(-j)建立作业队列

step2 并行执行

  • 启动子进程:根据 CPU 核心数或用户设定,同时运行多个命令实例
  • 替换参数:将输入行动态填充到命令模板中的 {} 位置

step3 输出聚合

  • 缓冲输出:Parallel 会捕获每个作业的标准输出(stdout)和错误输出(stderr)
  • 原子打印:只有当一个作业彻底完成后,才将其输出一次性打印到屏幕,保证多线程环境下输出不会乱序交织

工作流程图:

INPUT (文件列表/参数)
|
[ 任务分发器 (Scheduler) ] <- 设定并发数 (-j)
/ | \
Core1 Core2 Core3 (并行处理)
| | |
Buff1 Buff2 Buff3 (输出缓冲)
\ | /
[ 输出聚合器 (Aggregator) ]
|
OUTPUT (整洁有序的输出)

(1) 智能核心利用

默认情况下,Parallel 会检测当前机器的 CPU 物理核心数,并以此作为并发度。

(2) 替换字符串

类似于 awk 的 $1,Parallel 使用 {} 及其变体来代表当前的输入参数。

(3) 输出保序(可选)

  • 默认情况下,谁先做完谁先输出
  • 使用 -k 时,Parallel 会暂存先完成的任务结果,强制按输入顺序输出

Parallel 有一套强大的替换字符串,用于在命令中灵活地处理文件名、路径和扩展名。

假设当前输入参数为 data/archive.tar.gz:

符号 含义 对应示例值
{} 完整输入行 data/archive.tar.gz
{.} 去掉扩展名 data/archive.tar
{/} 基名 archive.tar.gz
{//} 目录名 data
{/.} 基名 + 去掉扩展名 archive.tar
{#} 作业序列号 1(如果是第一个任务)
{%} 作业槽位号 1(如果在第 1 个 CPU 核心上运行)

替换符示例:

Terminal window
# {} 完整参数
parallel echo {} ::: A B C
# {.} 格式转换:将所有 .png 转换为 .jpg
ls *.png | parallel convert {} {.}.jpg
# {/} 提取文件名:将 /var/log/ 下的文件复制到当前目录
ls /var/log/*.log | parallel cp {} ./backup-{/}
# {#} 生成序号:给输出文件编号
ls *.jpg | parallel mv {} img_{#}.jpg
选项 含义与作用
-j N 并发数;N 可以是数字或百分比(如 100%),默认是 CPU 核心数
-k 保持顺序(Keep Order);强制输出顺序与输入顺序一致
--dry-run 空运行;只打印将要执行的命令,不实际运行
--bar 显示进度条;显示完成度、速度和预计剩余时间
--joblog file 记录日志;将每个任务的退出码、运行时间记录到文件
--resume 断点续传;配合 --joblog 使用,跳过已成功的任务
--pipe 分块处理;将输入(通常是大文件)分块后传给命令
-S host 远程执行(SSH);在远程服务器上运行命令

选项示例:

Terminal window
# -j:限制同时只跑 2 个任务
ls *.txt | parallel -j 2 gzip {}
# --dry-run:在执行删除或覆盖操作前先预览
find . -name "*.tmp" | parallel --dry-run rm {}
# --bar:显示进度条
seq 1000 | parallel -j 20 --bar sleep 0.1
# --joblog & --resume:任务管理与断点续传
ls *.mp4 | parallel --joblog my.log ffmpeg -i {} {.}.avi
# 中断后再次运行,自动跳过已完成的任务
ls *.mp4 | parallel --joblog my.log --resume ffmpeg -i {} {.}.avi

多源输入(笛卡尔积)

使用多个 ::: 传入多组参数:

Terminal window
# 类似于:for x in 1 2; do for y in A B; do echo $x $y; done; done
parallel echo {1} {2} ::: 1 2 ::: A B

输出:

Terminal window
1 A
1 B
2 A
2 B

对应链接(类似 Python 的 zip)

使用 :::+ 实现一一对应:

Terminal window
parallel echo {1} {2} ::: 1 2 :::+ A B

输出:

Terminal window
1 A
2 B

文件作为输入

使用 :::: 直接读取文件内容作为参数:

Terminal window
parallel ping -c 1 {} :::: hosts.txt

在 Parallel 中使用管道符 (|)、重定向符 (>, >>) 或命令分隔符 (;, &&) 时,必须引用整个命令字符串,否则当前 Shell 会优先解释这些符号,导致逻辑错误。

Terminal window
# 推荐写法:使用双引号包裹整个命令串
parallel "command {} | chain_cmd > output_{}"

管道与重定向:解压文件 -> 搜索内容 -> 保存到独立文件

错误写法:

Terminal window
ls *.gz | parallel zcat {} | grep "Error" > {}.log

当前 Shell 会先启动 grep "Error",然后把 parallel 的所有输出都喂给它,grep 是串行处理的。

正确写法:

Terminal window
ls *.gz | parallel "zcat {} | grep 'Error' > {.}.log"

命令序列:先创建目录,再移动文件

错误写法:

Terminal window
ls *.txt | parallel mkdir -p tmp; mv {} tmp/

正确写法:

Terminal window
ls *.txt | parallel "mkdir -p tmp; mv {} tmp/"

当需要执行多步操作或包含逻辑判断时,将逻辑封装进函数,导出函数,然后让 Parallel 调用:

批量生成缩略图(跳过已存在的)

Terminal window
make_thumbnail() {
local img="$1"
local filename=$(basename "$img")
local base="${filename%.*}"
local thumb="thumbnails/${base}_thumb.jpg"
echo "Processing $img..."
if [ -f "$thumb" ]; then
echo "Skip $img (already has thumbnail)"
return
fi
mkdir -p thumbnails
convert -resize 200x "$img" "$thumb"
echo "Done: $thumb"
}
export -f make_thumbnail
parallel make_thumbnail ::: *.jpg