【问题标题】:posix pipe as a work queueposix 管道作为工作队列
【发布时间】:2012-04-12 06:43:27
【问题描述】:

我见过的工作队列的正常实现涉及互斥锁和条件变量。

消费者:

A) Acquires Lock
B) While Queue empty
      Wait on Condition Variable (thus suspending thread and releasing lock)
C) Work object retrieved from queue
D) Lock is released
E) Do Work
F) GOTO A

制作人:

A) Acquires Lock
B) Work is added to queue
C) condition variable is signaled (potentially releasing worker)
D) Lock is released

我一直在浏览一些代码,并且看到了一个使用 POSIX 管道的实现(我以前没有见过这种技术)。

消费者:

A) Do select on pipe (thus suspending thread while no work)
B) Get Job from pipe
C) Do Work
D) GOTO A

制作人:

A) Write Job to pipe.

由于生产者和消费者是同一个应用程序中的线程(因此它们共享相同的地址空间,因此它们之间的指针是有效的);作业作为工作对象(C++ 对象)的地址写入管道。因此,必须从管道中写入/读取的只是一个 8 字节地址。

我的问题是:

  • 这是一种常见的技术吗(我是否已经避开了这个),有哪些优点/缺点?

我的好奇心被激起了,因为管道技术不涉及任何可见的锁定或信号(它可能隐藏在选择中)。所以我想知道这是否会更有效率?

编辑:

基于@Maxim Yegorushkin 回答中的 cmets。

实际上,此场景中的“生产者”参与了来自大量源的大量并行 IO。所以我怀疑原作者虽然很希望这个线程在任何情况下都不会被阻塞,但也不想在“Producer”线程中付出高昂的代价。

【问题讨论】:

  • 其实在管道的情况下,可以跳过A。如果您在 select() 中阻塞单个文件描述符,您也可以调用 read() 并在那里阻塞。
  • @Rob。实际上有两个管道,因为消费者将响应对象写入另一个管道(对于生产者)。所以它会选择;在输入管道上读取并可能在输出管道上写入(假设我们已经排队等待外出)。
  • 这是否仅适用于低于内核管道大小的有效负载大小?根据架构,这可能是 512 字节、4k、16k 等。超过此大小的有效负载可能跨越多个不同的消费者?
  • @Xepoch:有效载荷(用于管道)为 8 个字节(或指针的大小)。由于生产者和消费者都是同一进程中的线程,因此代码通过管道传递指针,而实际数据位于动态分配的 C++ 对象中。
  • @lok​​i,正确,但这只是因为内核将允许一些最大的“缓冲区”以原子/串行方式进入管道(可能在上下文切换之前)。重点是让你的有效载荷保持在低水平。

标签: c++ message-queue pipe


【解决方案1】:

正如这里已经提到的,人们使用管道作为队列来避免阻塞非阻塞 I/O 线程中的条件变量(即在select/epoll 上处理多个套接字和块的线程) .如果 I/O 线程在条件变量或互斥体上阻塞,它就不能再进行非阻塞 I/O。

有人说写入管道涉及系统调用,并且当线程间事件量很高时可能会增加延迟。这仅适用于简单的基于管道的队列实现。

高级实现使用作业/事件的无锁链表,并且仅当将第一个作业添加到列表中时,才会写入管道以将目标 I/O 线程从阻塞 epoll 调用中唤醒(主要使用管道作为边缘触发的通知机制,但不用于传递指向作业/事件的指针)。因为唤醒一个线程需要几微秒,在此期间可能有更多的作业/事件发布到该线程的事件队列,但每个后续事件都不需要写入管道,直到稍后 I/O线程唤醒并消耗队列中的所有事件。此外,在较新的 Linux 内核中,可以使用更快的eventfd 代替管道来唤醒 I/O 线程。

【讨论】:

  • 这个管道是一个“posix管道”。是不是很幼稚?
  • 基于 POSIX 管道的队列实现可能是幼稚的或高级的。 POSIX 管道就是这样。
【解决方案2】:

我已经做到了。这是老式的,但它有效。

我这样做的原因是我需要唤醒同一个线程来完成任务或从另一个源读取输入,因此涉及到 select()。

【讨论】:

    【解决方案3】:

    这是因为select 及其结构。正如您在手册页中看到的那样

    select() 和 pselect() 允许程序监视多个文件描述符,等待一个或多个文件描述符“准备好”用于某种 I/O 操作(例如,可能的输入)。如果可以在不阻塞的情况下执行相应的 I/O 操作(例如 read(2)),则认为文件描述符已准备就绪。

    上面的关键是'等待一个或多个FD准备好'。那是两个线程之间的同步点。

    【讨论】:

    • 但是在这种情况下只有一个管道。 (实际上有两个(一个读/一个写)工作人员将响应放在返回管道上供生产者读取(但我们不想阻止写入或读取)。
    【解决方案4】:

    我认为答案是管道技术的性能不如它涉及的系统调用那么好,系统调用相对昂贵。但这确实意味着所有棘手的锁定、睡眠和醒来都会为您处理好。

    我自己都使用过,但管道仅用于偶尔的非性能关键应用程序。

    编辑:我想我不妨提出标准建议,因为没有人提出任何明确权威的 cmets。

    标准建议是:尝试两者并对其进行基准测试。这是找出哪个性能更好的一种真正方法...

    【讨论】:

    • 根据代码中的 cmets,使用管道技术是因为它更有效(因为没有锁)。现在我没有指标来支持这一点(这就是我问这个问题的原因)。如果您有任何关于您的假设的参考资料,将会很感兴趣。
    猜你喜欢
    • 2016-07-10
    • 2019-03-27
    • 2018-06-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-28
    • 1970-01-01
    • 2018-09-11
    相关资源
    最近更新 更多