使用 GNU Parallel 在 Linux 命令行中完成更多工作

将您的计算机变成一个多任务处理的强大工具。
335 位读者喜欢这篇文章。
Avoiding data disasters with Sanoid

Opensource.com

您是否曾经觉得您的电脑没有达到它应有的速度?我过去常常有这种感觉,然后我发现了 GNU Parallel。

GNU Parallel 是一个用于并行执行作业的 shell 实用程序。它可以解析多个输入,从而同时针对多组数据运行您的脚本或命令。您终于可以使用所有的 CPU 了!

如果您曾经使用过 xargs,您就已经知道如何使用 Parallel。如果您没有,那么这篇文章将教您,以及许多其他的用例。

安装 GNU Parallel

GNU Parallel 可能没有预装在您的 Linux 或 BSD 电脑上。从您的软件仓库或端口集合中安装它。例如,在 Fedora 上

$ sudo dnf install parallel

或者在 NetBSD 上

# pkg_add parallel

如果所有方法都失败了,请参考项目主页

从串行到并行

正如它的名字所暗示的,Parallel 的优势在于它可以并行运行作业,而不是像我们中的许多人仍然做的那样,按顺序运行。

当您针对多个对象运行一个命令时,您本质上是在创建一个队列。一定数量的对象可以被命令处理,而所有其他的对象只是站在旁边等待轮到它们。这是低效的。给定足够的数据,总是会有一个队列,但是为什么不拥有很多小的队列,而不是只有一个队列呢?

想象一下,您有一个文件夹,里面装满了您想要从 JPEG 转换成 PNG 的图片。有很多方法可以做到这一点。有一种手动方式是在 GIMP 中打开每一张图片,然后将它导出为新的格式。这通常是最糟糕的方式。这不仅耗时,而且耗费劳力。

这个主题的一个非常简洁的变体是基于 shell 的解决方案

$ convert 001.jpeg 001.png
$ convert 002.jpeg 002.png
$ convert 003.jpeg 003.png
... and so on ...

当您第一次学习它时,这是一个很棒的技巧,而且起初这是一个巨大的进步。不需要 GUI 和不断的点击。但它仍然很耗费劳力。

更好的是

$ for i in *jpeg; do convert $i $i.png ; done

至少,这会让作业开始运行,并让您可以去做更多富有成效的事情。问题是,这仍然是一个串行过程。一个图像被转换,然后队列中的下一个图像加入转换,等等,直到队列被清空。

使用 Parallel

$ find . -name "*jpeg" | parallel -I% --max-args 1 convert % %.png

这是两个命令的组合:find 命令,它收集您想要操作的对象,以及 parallel 命令,它对对象进行排序,并确保所有内容都按要求进行处理。

  • find . -name "*jpeg" 查找当前目录中所有以 jpeg 结尾的文件。
  • parallel 调用 GNU Parallel。
  • -I% 创建一个占位符,称为 %,用来代表 find 传递给 Parallel 的任何东西。您使用这个占位符是因为否则您必须手动为 find 的每个结果编写一个新的命令,而这正是您想要避免的。
  • --max-args 1 限制 Parallel 从队列中请求新对象的速率。由于 Parallel 正在运行的命令只需要一个文件,因此您将速率限制为 1。如果您正在执行一个更复杂的需要两个文件的命令(例如 cat 001.txt 002.txt > new.txt),您将速率限制为 2。
  • convert % %.png 是您想要在 Parallel 中运行的命令。

这个命令的结果是,find 收集所有相关文件,并将它们传递给 parallel,后者启动一个作业,并立即请求队列中的下一个。Parallel 继续这样做,只要启动新的作业不会削弱您的电脑。当旧的作业完成后,它会用新的作业替换它们,直到提供给它的所有数据都被处理完毕。以前需要 10 分钟的时间,使用 Parallel 可能只需要 5 分钟或 3 分钟。

多个输入

只要您熟悉 findxargs(统称为 GNU Find Utilities,或 findutils),find 命令就是一个连接 Parallel 的极好通道。它提供了一个灵活的界面,许多 Linux 用户已经很熟悉它,如果您是新手,学习起来也很容易。

find 命令非常简单:您向 find 提供一个要搜索的目录的路径,以及您要搜索的文件名的一部分。使用通配符来扩大您的搜索范围;在这个例子中,星号表示任何东西,所以 find 会找到所有以字符串 searchterm 结尾的文件。

$ find /path/to/directory -name "*searchterm"

默认情况下,find 一次返回一个搜索结果,每个结果占一行。

$ find ~/graphics -name "*jpg"
/home/seth/graphics/001.jpg
/home/seth/graphics/cat.jpg
/home/seth/graphics/penguin.jpg
/home/seth/graphics/IMG_0135.jpg

当您将 find 的结果管道传输到 parallel 时,每一行的每一项都被视为 parallel 仲裁的命令的一个参数。另一方面,如果您需要在一条命令中处理多个参数,您可以拆分队列中的数据传递给 parallel 的方式。

这是一个简单的、不切实际的例子,我稍后会把它变成更有用的东西。只要您安装了 GNU Parallel,您就可以跟着这个例子一起做。

假设您有四个文件。将它们逐行列出,以准确查看您拥有什么

$ echo ada > ada ; echo lovelace > lovelace
$ echo richard > richard ; echo stallman > stallman
$ ls -1
ada
lovelace
richard
stallman

您想要将两个文件合并成第三个文件,其中包含这两个文件的内容。这要求 Parallel 访问两个文件,因此 -I% 变量在这种情况下不起作用。

Parallel 的默认行为基本上是不可见的

$ ls -1 | parallel echo
ada
lovelace
richard
stallman

现在告诉 Parallel 您想要每个作业获取两个对象

$ ls -1 | parallel --max-args=2 echo
ada lovelace
richard stallman

现在这些行已经被合并了。具体来说,ls -1两个结果一次性地传递给 Parallel。这对这项任务来说是正确的参数数量,但它们现在实际上是一个参数:"ada lovelace" 和 "richard stallman." 您真正想要的是每个作业两个不同的参数。

幸运的是,这个技术细节由 Parallel 本身解析。如果您将 --max-args 设置为 2,您将得到两个变量,{1}{2},它们代表参数的第一部分和第二部分

$ ls -1 | parallel --max-args=2 cat {1} {2} ">" {1}_{2}.person

在这个命令中,变量 {1} 是 ada 或 richard(取决于您查看哪个作业),而 {2}lovelacestallman。这些文件的内容被重定向符号用引号括起来重定向(引号从 Bash 中获取重定向符号,以便 Parallel 可以使用它),并放入名为 ada_lovelace.personrichard_stallman.person 的新文件中。

$ ls -1
ada
ada_lovelace.person
lovelace
richard
richard_stallman.person
stallman

$ cat ada_*person
ada lovelace
$ cat ri*person
richard stallman

如果您整天都在解析数百兆字节大小的日志文件,您可能会看到并行文本解析对您有多么有用;否则,这主要是一个演示性的练习。

然而,这种处理方式对于文本解析来说不仅仅是无价的。这是一个来自电影世界的真实例子。考虑一个包含视频文件和音频文件的目录,这些文件需要合并在一起。

$ ls -1
12_LS_establishing-manor.avi
12_wildsound.flac
14_butler-dialogue-mixed.flac
14_MS_butler.avi
...and so on...

使用相同的原理,可以创建一个简单的命令,以便文件并行合并

$ ls -1 | parallel --max-args=2 ffmpeg -i {1} -i {2} -vcodec copy -acodec copy {1}.mkv

蛮力。

所有这些花哨的输入和输出解析并不适合每个人的口味。如果您喜欢更直接的方法,您可以将命令扔给 Parallel,然后走开。

首先,创建一个文本文件,每行包含一个命令

$ cat jobs2run
bzip2 oldstuff.tar
oggenc music.flac
opusenc ambiance.wav
convert bigfile.tiff small.jpeg
ffmepg -i foo.avi -v:b 12000k foo.mp4
xsltproc --output build/tmp.fo style/dm.xsl src/tmp.xml
bzip2 archive.tar

然后将文件交给 Parallel

$ parallel --jobs 6 < jobs2run

现在,您的文件中所有的作业都以并行方式运行。如果存在的作业比允许的作业多,则 Parallel 会形成并维护一个队列,直到所有作业都运行完毕。

更多,更多

GNU Parallel 是一个强大而灵活的工具,它的用例远远超出本文的范围。它的手册页提供了您可以利用它做出的非常酷的事情的例子,从通过 SSH 进行远程执行到将 Bash 函数合并到您的 Parallel 命令中。甚至还有一个广泛的演示系列在 YouTube 上,因此您可以直接从 GNU Parallel 团队那里学习。GNU Parallel 的主要维护者也刚刚发布了该命令的官方指南,可从 Lulu.com 获取。

GNU Parallel 有能力改变您计算的方式,如果它没有做到这一点,它至少会改变您的计算机花费在计算上的时间。今天就试试吧!

标签
Seth Kenlon
Seth Kenlon 是一位 UNIX 极客,自由文化倡导者,独立多媒体艺术家和 D&D 爱好者。他曾在电影和计算机行业工作,通常同时进行。

9 条评论

非常有用的文章!我想提出一个建议:parallel 的第一个使用示例

find . -name "*jpeg" | parallel -I% --max-args 1 convert % %.png

可能会误导人们认为总是需要使用 -I 定义占位符。 parallel 有一个丰富的占位符集,适用于所有目的,应该使用它们。此外,在该示例中,PNG 文件名将是附加了 .png 的 JPEG 文件名。编写该示例的更自然的方式是

find . -name "*jpeg" | parallel --verbose --max-args 1 convert {} {.}.png

我认为 --verbose 非常重要,这样人们就可以看到 parallel 正在执行哪些命令行。

很棒的提示,谢谢!我不会添加它或更改文章,因为我不想在介绍新命令时引入太多的复杂性。我希望读者关注原则,而不是最大效率(这会引入复杂的语法)。

不过,您的评论是一个很好的补充。

回复 作者 Paulo Marcel C…

我冒险听起来很坚持,但是 ... 您引入了使用 -I% 的复杂性,因为它是不必要的。此外,它会导致错误的结果,因为最终的文件名不会按照预期替换扩展名,而是附加扩展名。

回复 作者 sethkenlon

更正:这是错误的

“幸运的是,这个技术细节由 Parallel 本身解析。如果您将 --jobs 设置为 2,您将得到两个变量,{1} 和 {2},它们代表参数的第一部分和第二部分

$ ls -1 | parallel --max-args=2 --jobs 2 cat {1} {2} ">" {1}_{2}.person"

变量 {1} 和 {2} 可用是因为 --max-args=2 而不是因为 --jobs=2。在这个例子中不需要 --jobs=2。

好眼力,谢谢。 你是对的; --jobs 实际上限制了生成的并行任务数量(如文章后面所述),它不会影响参数计数。

我已经更新了文本以反映您的更正。谢谢!

回复 作者 Paulo Marcel C…

我刚刚学习了 parallel 教程,我有一个最后的更正。 没有必要使用 --max-args=1,因为那是 parallel 的默认行为。 所以,第一个例子应该是

find . -name '*jpeg' | parallel convert {} {.}.png

再次或许听起来很固执,但是... parallel 的基本理念是一次处理一个参数:当你引入 --max-args 1 时,你会转移对它的基本工作方式的注意力。 第一次使用 parallel 的人会被误导,认为总是需要包含 --max-args。 正如您之前所说,最好避免在教程中引入不必要的复杂性,而引入不必要的选项就会造成这种情况。

回复 作者 sethkenlon

xargs 已经可以做到其中的一些了

find . -iname "*.png" | xargs -I FILE -n 1 --max-procs=4 cp -v FILE FILE.new

gnu-parallel 听起来像是更强大的版本。

知识共享许可协议本作品采用知识共享署名 - 相同方式共享 4.0 国际许可协议进行许可。
© . All rights reserved.