Bash
命令通常单线程运行,这意味着所有的处理工作只在单个 CPU 上执行。随着 CPU 规模的扩大以及核心数目的增加,这意味着只有一小部分的 CPU 资源用于处理任务,这样就造成了很大的资源浪费。 这种情况在进行多媒体转换(比如:图片和视频转换)以及数据压缩中经常遇到。
本文我们将介绍如何使用 GNU Parallel 程序在所有 CPU 核上并行地执行计算任务。
Parallel 介绍
GNU Parallel 是一种通用的并行化程序,可以在同一台机器上或在您具有 SSH 访问权限的多台机器上轻松并行运行作业。
如果要在 4 个 CPU 上运行 32 个不同的作业,并行化的一种直接方法是在每个 CPU 上运行8个作业。
https://upload-images.jianshu.io/upload_images/27460-5b784effa2396844.png
GNU Parallel 会在完成后生成一个新进程,并保持 CPU 处于活动状态,从而节省时间。
https://upload-images.jianshu.io/upload_images/27460-2901afa31279f8ff.png
Parallel 安装
- 通过包安装
- CentOS / RHEL
1 | $ yum install parallel |
- Ubuntu / Debian
1 | $ sudo apt install parallel |
- 通过脚本安装
1 | $ (wget -O - pi.dk/3 || curl pi.dk/3/) | bash |
Parallel 使用
Parallel 语法简介
1 | Usage: |
- Parallel 常用选项
1 | ::: 后面接参数。 |
Parallel 用法简介
- 输入源
GNU Parallel 的输入源支持文件、命令行和标准输入( Stdin 或 Pipe)。
- 以命令行做为输入源
1 | $ parallel echo ::: a b c d e | tee a.txt |
- 以 Stdin(标准输入)作为输入源
1 | $ cat a.txt | parallel echo |
- GNU Parallel 支持通过命令行指定多个输入源,它会生成所有的组合
1 | $ parallel echo ::: A B C ::: D E F | tee b.txt |
- 多个文件作为输入,此时多个文件中的内容也会像上面那样进行组合
1 | $ parallel -a a.txt -a b.txt echo |
- Stdin(标准输入)作为文件源中的一个,使用 -, 输出结果同上
1 | $ cat a.txt |parallel -a - -a b.txt echo |
- 使用
::::
代替-a
,后面可接多个文件名
1 | $ cat a.txt | parallel echo :::: - b.txt |
:::
和::::
可以同时使用,同样的输出结果也会进行组合
1 | $ parallel echo ::: a b :::: b.txt |
- 当然,若不想像上面那样进行组合,可使用 --xapply 参数从每一个源获取一个参数(或文件一行),这个参数有些类似 R 中的函数,具有广播作用。如果其中一个输入源的长度比较短,它的值会被重复。
1 | $ parallel --xapply echo ::: A B C ::: D E F |
- 改变参数分隔符
GNU Parallel 可以通过 --arg-sep
和 --arg-file-sep
指定分隔符替代 :::
或 ::::
,当这两个符号被其它命令占用的时候会特别有用。
1 | $ parallel -k --arg-sep ,,, echo ,,, a b ,,, c d | tee c.txt |
- 改变输入分隔符
GNU Parallel 默认把一行做为一个参数。使用 \n
做为参数定界符,可以使用 -d
改变。
1 | $ parallel -d b echo :::: a.txt |
- 提前结束和跳过空行
GNU Parallel 支持通过 -E
参数指定一个值做为结束标志。
1 | $ parallel -E stop echo ::: A B stop C D |
GNU Parallel 使用 --no-run-if-empty
来跳过空行。
1 | $ (echo 1; echo; echo 2) | parallel --no-run-if-empty echo |
- 构建命令行
如果 Parallel 之后没有给定命令,那么这些参数会被当做命令。
1 | $ parallel ::: ls 'echo foo' pwd |
此外,命令还可以是一个脚本文件,一个二进制可执行文件或一个 Bash
的函数(须用 export -f 导出函数)。
1 | $ echo "echo \$*" > s.sh |
- 替换字符串
GNU Parallel 支持多种替换字符串,默认使用 {},使用 -I
改变替换字符串符号 {}。
其最常见的字符串替换包括以下几种:
- {.},去掉扩展名
- {/},去掉路径,只保留文件名
- {//},只保留路径
- {/.},同时去掉路径和扩展名
1 | $ parallel echo ::: A/B.C ; parallel echo {} ::: A/B.C ; parallel -I ,, echo ,, ::: A/B.C |
同时,如果有多个输入源时,可以通过 {编号} 指定某一个输入源的参数。
1 | $ parallel --xapply echo {1} and {2} ::: A B ::: C D |
可以使用 / // /. 和 . 改变指定替换字符串
1 | $ parallel echo /={1/} //={1//} /.={1/.} .={1.} ::: A/B.C D/E.F |
位置可以是负数,表示倒着数。
1 | $ parallel echo 1={1} 2={2} 3={3} -1={-1} -2={-2} -3={-3} ::: A B ::: C D ::: E F |
- 按列输入和指定参数名
使用 --header
把每一行输入中的第一个值做为参数名。
1 | $ parallel --xapply --header : echo f1={f1} f2={f2} ::: f1 A B ::: f2 C D | tee d.txt |
使用 --colsep
把文件中的行切分为列,做为输入参数。
1 | $ perl -e 'printf "f1\tf2\nA\tB\nC\tD\n"' > tsv-file.tsv |
- 多参数
--xargs
会在一行中输入尽可能多的参数(与参数字符串长度有关),通过 -s
可指定一行中参数的上限。
1 | $ perl -e 'for(1..30000){print "$_\n"}' > num30000 |
为了获得更好的并发性,GNU Parallel 会在文件读取结束后再分发参数。
GNU Parallel 在读取完最后一个参数之后,才开始第二个任务,此时会把所有的参数平均分配到 4 个任务(如果指定了4个任务)。
第一个任务与上面使用 --xargs
的例子一样,但是第二个任务会被平均的分成 4 个任务,最终一共 5 个任务。
1 | $ cat num30000 | parallel --jobs 4 -m echo | wc -l |
将 1-10 分参数分配到4个任务可以看得更清晰。
1 | $ parallel --jobs 4 -m echo ::: {1..10} |
替换字符串可以是输出字符的一部分,使用 -m
参数表示每个 job 不重复输出 “背景”(context),-X
则与 -m
相反,会重复输出 “背景文本”,具体通过下面几个例子进行理解。
1 | $ parallel --jobs 4 echo pre-{}-post ::: A B C D E F G |
使用 -N
限制每行参数的个数,其中 -N0
表示一次只读取一个参数,且不输入这个参数(作为计数器来使用)。
1 | $ parallel -N4 echo 1={1} 2={2} 3={3} ::: A B C D E F G H |
- 引用
如果命令行中包含特殊字符,就需要使用引号保护起来。
Perl 脚本 'print "@ARGV\n"'
与 Linux 的 echo
的功能一样。
1 | $ perl -e 'print "@ARGV\n"' A |
使用 GNU Parallel 运行这条命令的时候,Perl 命令需要用引号包起来,也可以使用 -q
保护 Perl
命令。
1 | $ parallel perl -e 'print "@ARGV\n"' ::: This wont work |
- 去除空格
使用 --trim 去除参数两头的空格。
1 | $ parallel --trim r echo pre-{}-post ::: ' A ' |
- 控制输出
使用 --tag
以参数做为输出前缀,使用 --tagstring
修改输出前缀。
1 | $ parallel --tag echo foo-{} ::: A B C |
--dryrun
作用类似于 echo 。
1 | $ parallel --dryrun echo {} ::: A B C |
--verbose
则在运行之前先打印命令。
1 | $ parallel --verbose echo {} ::: A B C |
一般来说,GNU Parallel 会延迟输出,直到一组命令执行完成。使用 --ungroup
,可立刻打印输出已完成部分。
1 | $ parallel -j2 'printf "%s-start\n%s" {} {};sleep {};printf "%s\n" -middle;echo {}-end' ::: 4 2 1 |
使用 --ungroup
会很快,但会导致输出错乱,一个任务的行输出可能会被另一个任务的输出截断。像上例所示,第二行输出混合了两个任务:‘4-middle’ ‘2-start’。使用 --linebuffer
避免这个问题(稍慢一点)。
1 | 4-start |
强制使输出与参数保持顺序 --keep-order/-k
。
1 | $ parallel -j2 -k 'printf "%s-start\n%s" {} {};sleep {};printf "%s\n" -middle;echo {}-end' ::: 4 2 1 |
- 将输出保存到文件
GNU Parallel 可以把每一个任务的输出保存到文件中,临时文件默认保存在 /tmp 中,可以使用 --tmpdir
改变(或者修改 $TMPDIR)。
1 | $ parallel --files ::: A B C |
输出文件可以有结构的保存 --results
,输出文件不仅包含标准输出(stdout)也会包含标准错误输出(stderr)。
1 | $ parallel --results outdir echo ::: A B C |
在使用多个变量的时候会显得很有用。
1 | # --header : will take the first value as name and use that in the directory structure. |
- 控制执行
使用 --jobs/-j
指定并行任务数。
使用 64 个任务执行 128 个休眠命令。
1 | $ time parallel -N0 -j64 sleep 1 ::: {1..128} |
默认情况下并行任务数与 Cpu 核心数相同, 所以这条命令会比每个 Cpu 两个任务的耗时多一倍。
1 | $ time parallel -N0 sleep 1 ::: {1..128} |
使用 --jobs 0
表示执行尽可能多的并行任务。
1 | $ time parallel -N0 --jobs 0 sleep 1 ::: {1..128} |
除了基于 Cpu 使用率之外,也可以基于 Cpu 数。
1 | $ time parallel --use-cpus-instead-of-cores -N0 sleep 1 ::: {1..128} |
- 交互
通过使用 --interactive
在一个任务执行之前让用户决定是否执行。
1 | $ parallel --interactive echo ::: 1 2 3 |
- 耗时
当 job 有大量的 IO 操作时,为避免“惊群效应”,可使用 --delay
参数指定各个 job 开始的时间间隔。
1 | $ parallel --delay 2.5 echo Starting {}\;date ::: 1 2 3 |
若已知任务超过一定时间未反应则为失败则可以通过 --timeout
指定等待时间避免无谓的等待。
GNU Parallel 能计算所有任务运行时间的中位数,因此可以指定时间为中位数的倍数关系。
1 | $ parallel --timeout 4.1 sleep {}\; echo {} ::: 2 4 6 8 |
- 显示任务进度信息
GNU Parallel 有多种方式可用来动态的显示任务进度信息,如:
1 | $ parallel --eta sleep ::: 1 3 2 2 1 3 3 2 1 |
使用 --joblog
参数能够生成各个任务的日志文件。
1 | $ parallel --joblog /tmp/log exit ::: 1 2 3 0 |
通过 --resume-failed
参数可以重新运行失败的任务。
--retry-failed
的作用与 --resume-failed
类似,只是 --resume-failed
从命令行读取失败任务,而 --retry-failed
则是从日志文件中读取失败任务。
1 | $ parallel --resume-failed --joblog /tmp/log exit ::: 1 2 3 0 0 0 |
- 终止任务
GNU Parallel 支持在某一情况下(如第一个失败或成功时,或者 20% 任务失败时)终止任务。
终止任务又有两种类型:
- 其一为立即终止(通过
--halt now
指定),杀死所有正在运行的任务并停止生成新的任务。 - 其二为稍后终止(通过
--halt soon
指定),停止生成新任务并等待正在运行任务完成。
1 | $ parallel -j2 --halt soon,fail=1 echo {}\; exit {} ::: 0 0 1 2 3 |
GNU Parallel 还支持在任务失败后重试运行 --retries
。
1 | $ parallel -k --retries 3 'echo tried {} >>/tmp/runs; echo completed {}; exit {}' ::: 1 2 0 |
关于终止信号的高级用法参考官方入门文档。
- 资源限制
GNU Parallel 能够在开始一个新的任务前检查系统的负载情况防止过载(通过 --load
可指定负载),同时还能检查系统是否使用了交换空间 Swap(通过 --noswap
限制使用 Swap)。
1 | $ parallel --load 100% echo load is less than {} job per cpu ::: 1 |
同时,对于某些占用内存较多的程序,Parallel 会检查内存只有内存满足时才启动任务(通过 --memfree
指定需要内存大小),而且在启动任务后内存不够 50% 时会杀掉最新开始的任务,直到这个任务完成再重新开始那些杀死的任务。
1 | $ parallel --memfree 1G echo will run if more than 1 GB is ::: free |
还可以通过 --nice
来指定任务的优先级。
1 | $ parallel --nice 17 echo this is being run with nice -n ::: 17 |
- 远程操作
可使用 -S host
来进行远程登陆。
1 | $ parallel -S username@$SERVER1 echo running on ::: username@$SERVER1 |
- 文件传输
GNU Parallel 文件传输使用的是 Rsync。
1 | $ echo This is input_file > input_file |
更多远程操作参见官方入门文档。
- –pipe
--pipe
参数使得我们可以将输入(stdin)分为多块(block),然后分配给多个任务多个 Cpu 以达到负载均衡,最后的结果顺序与原始顺序一致。
使用 --block
参数可以指定每块的大小,默认为 1M。
1 | $ perl -e 'for(1..1000000){print "$_\n"}' > num1000000 |
如果不关心结果顺序,只想要快速的得到结果,可使用 --round-robin
参数。
没有这个参数时每块文件都会启动一个命令,使用这个参数后会将这些文件块分配给 job 数任务(通过 --jobs
进行指定)。若想分配更为均匀还可同时指定 --block
参数。
1 | $ cat num1000000 | parallel --pipe -j4 --round-robin wc |
Parallel 使用实例
下面这些实际的使用 Parallel 的例子可能会更容易理解一些。
- 使用 Parallel 来进行 JPEG 压缩
下面是一个普通的 find
命令,用来找出当前目录中的所有 .jpg 文件,然后通过 MozJPEG 包中提供的图像压缩工具 cjpeg
对其进行处理。
1 | $ find . -type f -name "*.jpg" -exec cjpeg -outfile LoRes/{} {} ';' |
总共耗时 0m44.114s。从 top 运行结果可以看到,虽然有 8 个核可用,但实际只有单个线程在用单个核。
https://img.linux.net.cn/data/attachment/album/201712/22/223442u13nq87a61an9z9q.png
下面用 Parallel 来运行相同的命令。
1 | $ find . -type f -name "*.jpg" | parallel cjpeg -outfile LoRes/{} {} |
这次压缩所有图像的时间缩减到了 0m10.814s。从 top 运行结果可以看到,所有 CPU 核都满负荷运行,有 8 个线程对应使用 8 个 CPU 核。
https://img.linux.net.cn/data/attachment/album/201712/22/223519h7n30u47omdqhzmu.png
参考文档
https://www.google.com
http://t.cn/E6KnjtP
http://t.cn/E6KB5Rr
http://t.cn/E69yv3g
http://t.cn/Rm9X2WC
http://t.cn/E6Wr74r