并行性的一个问题是同步。它是性能杀手,很糟糕,如果可能的话应该避免。
运维架构
让我们看看你的架构:
+--------------+--------------+
| Input 1 | Input 2 |
+--------------+--------------+
| QUEUE A |
+--------------+--------------+
| Scrub 1 | Scrub 2 |
+--------------+--------------+
| QUEUE B |
+---------+---------+---------+
| Compare | Compare | Compare |
+---------+---------+---------+
讨论
队列 A 必须跨四个线程同步;穿过 5-6 的队列 B。任何时候只有一个线程可以访问队列,所以大部分时间你的线程将等待,不工作!
并行管道架构
稍微不同的架构可能如下所示:
+-----------+ +-----------+
| Input 1 | | Input 2 |
+-----------+ +-----------+
| QUEUE 1A | | QUEUE 2A |
+-----------+ +-----------+
| Scrub 1 | | Scrub 2 |
+-----------+ +-----------+
| QUEUE 1B | | QUEUE 2B |
+-----+-----+ +-----+-----+
| Cmp | Cmp | | Cmp | Cmp |
+-----+-----+ +-----+-----+
讨论
在这里,A 队列只与两个线程相关联(-> 等待较少),B 队列只与三个线程相关联。对于类似的输入大小/复杂性,这种架构应该执行得更快。如果 Input 2 相当短,整个 Pipeline 2 将在 Pipeline 1 完成一半之前运行。但是,它比为每个管道使用单个进程要好得多。
草坪洒水器架构
概念
更好的架构会尝试将进程的输出分布到多个队列中。(相反,当队列为空时,让线程从多个队列中获取输入是不好的。)
每个队列写入应该转到不同的队列:
+-----------+ +-----------+
| Input 1 | | Input 2 |
+-----------+ +-----------+
| \ / |
+-----------+ +-----------+
| QUEUE 1A | | QUEUE 2A |
+-----------+ +-----------+
| Scrub 1 | | Scrub 2 |
+-----------+ +-----------+
/ | \ \ / / | \
+-------+-------+-------+-------+
| Q. 1B | Q. 2B | Q. 3B | Q. 4B |
+-------+-------+-------+-------+
| Cmp | Cmp | Cmp | Cmp |
+-------+-------+-------+-------+
这可以确保每个线程具有相同的工作负载,但不能确保所有线程同时完成。
讨论
所有队列在 3 个线程之间共享。问题是两个线程在写入队列时会相互阻塞。如果 Queue 写入访问之间的时间明显大于写入持续时间,这应该没有问题,否则可以混入第二种架构。
因此,这种架构是否有意义取决于您的确切要求。
对于均匀大小的输入,它的速度较慢,但在不规则输入上表现更好。
附录
实施时:
使用什么框架是次要的架构。如果您只传递文本字符串,我强烈建议您使用管道。如果您必须传递 Perl 数据类型或对象,您可能必须接受使用真实队列的额外开销:将非共享变量添加到队列时,还必须进行深层复制(请参阅@Leon Timmermans 答案)到所有同步开销。
关于可扩展性:
架构 1 和 3 的线程数不固定。我强烈建议使用这种灵活性来对不同的组合进行基准测试。经验法则是您应该使用n到2n 个线程,其中 n 是处理器(或硬件线程)的数量。这可以看作是一个阶段的线程的最大合理数。除此之外,您只会受到内存损失而没有加速。当一个阶段处理输入的速度比提供的速度快时,可能会更早地达到性能饱和点。