并行处理Linux命令
1. 简介
Linux 中有许多常见任务我们可能需要考虑并行运行,例如:
- 下载大量文件
- 在具有多个 CPU 内核的机器上编码/解码大量图像
- 使用许多不同的参数进行计算并存储结果
当然,我们可以在不使用并行化的情况下完成所有这些任务。但是如果我们在几个并行进程中处理每个文件、连接或计算,我们可以在速度方面拥有很大的优势。幸运的是,Linux 系统中有多种强大的并行化命令行工具可以帮助我们实现这一目标。
在本教程中,我们将了解如何使用 Bash *&*运算符、xargs 和GNU并行 在 Linux 命令行上实现并行化。
2. 示例任务
首先,让我们创建一个并行运行的简单脚本。
让我们创建一个名为*./process*的文件,其中包含以下内容:
#!/bin/bash
echo "started processing $*.."
sleep $((2 + RANDOM % 3)); echo finished processing "$*";
该脚本将伪造一个需要 2 到 5 秒才能完成的实际过程。让我们让它可执行以便能够使用它:
$ chmod +x ./process
3. 使用 &
作为并行运行命令的基本方法,我们可以使用内置的Bash *&*运算符 来异步运行命令,这样 shell 在继续执行下一个命令之前不会等待当前命令完成:
$ ./process 1 &
$ ./process 2 &
这将创建两个进程,它们将在基本相同的时刻开始并并行运行。因为我们在示例脚本中引入了随机睡眠时间,所以输出可能如下所示:
[1] 25254
[2] 25255
started processing 1..
started processing 2..
finished processing 2
finished processing 1
[1]- Done ./process 1
[2]+ Done ./process 2
**显然,我们可以使用这种方法来运行许多并行进程。但是如果我们有很多任务——例如,要转换一百张图像——我们不想一次启动所有一百个任务,而是分批处理它们以更好地利用我们的核心。**为了实现这一点,我们需要等待一些任务完成,然后再开始其他任务。
3.1. 使用wait&
默认情况下,wait 命令将等待所有子进程退出。因此,使用wait命令,我们可以运行批量操作:
$ echo "starting batch 1.."
$ ./process 1.jpg &
$ ./process 2.jpg &
$ ./process 3.jpg &
$ wait
$ echo "starting batch 2.."
$ ./process 4.jpg &
$ ./process 5.jpg &
$ ./process 6.jpg &
$ wait
$ echo "finished"
**但是,这种方法有一个很大的缺点。为了有效地利用我们的 CPU 内核,我们希望在正在运行的进程结束后立即启动一个新进程。**但是使用此解决方案,我们不会在上一批中的所有任务完成之前启动新流程。为了克服这个限制,我们可以使用xargs。
4. 使用xargs
/xargs 是一个命令行工具,可以帮助我们运行带有从标准输入解析的参数的命令。它还可以为我们并行化我们的任务。
让我们尝试之前使用 & 的输入,但这次使用xargs:
$ echo '1.jpg 2.jpg 3.jpg 4.jpg 5.jpg 6.jpg' | xargs -n 1 -P 3 ./process
started processing 1.jpg..
started processing 2.jpg..
started processing 3.jpg..
finished processing 1.jpg
finished processing 2.jpg
started processing 4.jpg..
started processing 5.jpg..
finished processing 3.jpg
started processing 6.jpg..
finished processing 5.jpg
finished processing 4.jpg
finished processing 6.jpg
**一旦进程完成,xargs会立即创建下一个进程。我们使用-n参数指定每次调用的参数数量,使用-P参数指定并行任务的数量。
4.1. 使用替换
如果我们正在使用的可执行文件要求我们将参数放在某个特定位置而不是直接将它们附加在可执行文件名称之后,我们可以使用替换。
让我们尝试一下:
$ args="1\n2\n3\n4\n5\n6"
$ echo -e $args | xargs -I "{}" -P 2 ./process {}.jpg
started processing 1.jpg..
started processing 2.jpg..
finished processing 2.jpg
started processing 3.jpg..
finished processing 1.jpg
started processing 4.jpg..
finished processing 3.jpg
started processing 5.jpg..
finished processing 5.jpg
started processing 6.jpg..
finished processing 4.jpg
finished processing 6.jpg
4.2. 使用换行符处理参数
如果我们要在流程中使用的参数包括换行符,我们可以使用空字符 (\0) 分隔的输入流。例如,使用find 命令,我们可以使用-print0标志将输出设置为空分隔而不是换行分隔:
$ find . -print0 | xargs -0 -n 2 -P 2 ./process
由于参数现在以空分隔,我们可以确定输入中的换行符将被保留。
5. 使用 GNUparallel
GNUparallel是可用于运行并行任务的最先进的命令行工具之一。它具有许多功能,包括使用ssh在多台机器上远程分发和运行任务的能力。
5.1.基本用法
parallel的基本用法与xargs非常相似。实际上,对于简单的情况,我们可以将它与xargs互换使用。 我们试试看:
$ args="1\n2\n3\n4\n5\n6"
$ echo -e $args | parallel --ungroup --jobs 3 ./process
started processing 1..
started processing 2..
started processing 3..
finished processing 1
finished processing 2
finished processing 3
started processing 4..
started processing 5..
started processing 6..
finished processing 5
finished processing 4
finished processing 6
–jobs参数与 xargs 命令的 -P 参数相同,它确定同时运行的并行作业的最大数量。
默认情况下,parallel只会在进程完成后打印进程的输出。–ungroup标志禁用此功能。我们可以使用它来查看命令运行时的实际执行顺序。
我们也可以通过命令行提供输入参数。让我们尝试运行它以获得与上面相同的输出:
$ parallel --ungroup --jobs 3 ./process ::: 1 2 3 4 5 6
在提供命令行参数时,我们可以使用 :::(三个冒号)直接提供参数,使用 ::::(四个冒号)从文件中提供参数。 让我们看一个从文件提供输入的示例:
$ args="1\n2\n3\n4\n5\n6"
$ echo -e $args > input.txt
$ parallel --ungroup --jobs 3 ./process :::: input.txt
输出将与上述类似。
5.2. 运行多个来源的组合
我们可以使用parallel来为两个源的每种可能组合运行任务。 让我们尝试两个示例源:
$ parallel --ungroup ./process ::: 1 2 3 ::: 1 2 3
started processing 1 1..
started processing 1 2..
started processing 1 3..
started processing 2 1..
started processing 2 2..
started processing 2 3..
started processing 3 1..
started processing 3 2..
finished processing 1 1
finished processing 1 2
finished processing 2 1
finished processing 2 2
finished processing 3 1
started processing 3 3..
finished processing 1 3
finished processing 3 2
finished processing 2 3
finished processing 3 3
5.3. 链接源
如果我们不想为每个可能的组合运行,而是希望将它们一个接一个地“链接”,我们将使用*–link*标志。让我们尝试使用两种不同的输入源:
$ parallel --link --ungroup ./process ::: 1 2 3 ::: 1 2 3
started processing 1 1..
started processing 2 2..
started processing 3 3..
finished processing 1 1
finished processing 3 3
finished processing 2 2
5.4. 替换字符串
就像在xargs中一样,我们可以parallel使用替换字符串。默认替换字符串是 {}。 让我们用前缀试试:
$ parallel --ungroup ./process item-{} ::: 1 2 3
started processing item-1..
started processing item-2..
started processing item-3..
finished processing item-1
finished processing item-2
finished processing item-3
其他替换字符串对输入进行不同类型的操作。例如,{.} 将从参数中删除扩展名:
$ parallel --ungroup ./process {.} ::: 1.jpg 2.jpg 3.jpg
started processing 1..
started processing 2..
started processing 3..
finished processing 2
finished processing 1
finished processing 3
如果我们想为每个命令使用多个不同的变量,我们也可以使用特殊的替换字符串来做到这一点:
$ parallel --ungroup --link ./process {1}.jpg {2}.jpg {3}.jpg ::: 1 2 3 ::: 4 5 6 ::: 7 8 9
started processing 1.jpg 4.jpg 7.jpg..
started processing 2.jpg 5.jpg 8.jpg..
started processing 3.jpg 6.jpg 9.jpg..
finished processing 1.jpg 4.jpg 7.jpg
finished processing 2.jpg 5.jpg 8.jpg
finished processing 3.jpg 6.jpg 9.jpg
5.5. 从文件列中读取输入
我们可以从文本文件的不同列中读取输入。让我们尝试使用制表符分隔的文本文件:
$ args="1\t4\n2\t5\n3\t6"
$ echo -e $args > input_cols.txt
$ parallel --colsep '\t' --ungroup ./process [{1}] [{2}] :::: input_cols.txt
started processing [1] [4]..
started processing [2] [5]..
started processing [3] [6]..
finished processing [2] [5]
finished processing [3] [6]
finished processing [1] [4]
5.6. 保存输出
我们可以使用–files标志将每个进程的输出保存到文件中:
$ parallel --files --link ./process ::: 1 2 3 ::: 1 2 3
/tmp/parnqFzp.par
/tmp/parSv0nW.par
/tmp/parGyNbz.par
这将创建*.par文件,其中我们的命令输出作为内容。
如果我们想要一个更友好的目录结构,我们可以使用*-results和-header*参数将结果写入层次结构中的文件夹。 让我们运行一个命令来生成目录树:
$ parallel --results outdir --link ./process ::: a b c ::: d e f
现在,让我们使用tree 命令检查输出:
outdir
└── 1
├── a
│ └── 2
│ └── d
│ ├── seq
│ ├── stderr
│ └── stdout
├── b
│ └── 2
│ └── e
│ ├── seq
│ ├── stderr
│ └── stdout
└── c
└── 2
└── f
├── seq
├── stderr
└── stdout
parallel根据参数位置和值生成目录结构。
5.7. 进度信息
我们还可以parallel显示基于当前任务运行的剩余时间的估计:
$ parallel --eta --colsep '\t' --ungroup ./process [{1}] [{2}] :::: input_cols.txt
started processing [1] [4]..
started processing [2] [5]..
Computers / CPU cores / Max jobs to run
1:local / 8 / 3
started processing [3] [6]..
Computer:jobs running/jobs completed/%of started jobs/Average seconds to complete
ETA: 0s Left: 3 AVG: 0.00s local:3/0/100%/0.0s finished processing [1] [4]
ETA: 0s Left: 2 AVG: 0.00s local:2/1/100%/3.0s finished processing [2] [5]
ETA: 0s Left: 2 AVG: 0.00s local:2/1/100%/3.0s finished processing [3] [6]
ETA: 0s Left: 1 AVG: 0.00s local:1/2/100%/1.5s
ETA: 0s Left: 0 AVG: 0.00s local:0/3/100%/1.0s
5.8. 在远程机器上运行并行任务
我们可以通过ssh在远程机器上运行并行任务。
假设我们可以使用添加到系统中的用户名和ssh密钥访问host1和host2 。让我们尝试一下:
$ parallel -S host1 -S host2 echo ::: running on remote hosts
running
on
remote
hosts
将运行每个命令的主机和执行顺序将随每次运行而随机更改。