多线程环境中的扩展 OVERLAPPED 对象池:在何处以及如何有效地使用锁定

Posted

技术标签:

【中文标题】多线程环境中的扩展 OVERLAPPED 对象池:在何处以及如何有效地使用锁定【英文标题】:Pool of extended OVERLAPPED objects in a multithreaded environment: where and how to use locking efficiently 【发布时间】:2014-06-18 10:26:40 【问题描述】:

在 C++ 中,我有一个 Stream 对象,它在 Windows 上抽象了一个 HANDLE,我还有各种派生对象,例如 FileTcpSocketUdpSocketPipe直接派生自Stream 对象,然后我还有一个RequestIo 对象,它是我自己的扩展OVERLAPPED 对象的版本,即RequestIo 直接继承自OVERLAPPED 结构。 从现在开始,说RequesIo 和说OVERLAPPED 是一样的。

RequestIo 对象中,我存储了一些无法存储在单个OVERLAPPED 结构中的有用信息,例如标志、用户指针等。 在那里我还存储了一个指向下一个RequestIo 对象的指针,以便拥有这些对象的侵入式链表。

然后,Stream 对象有 2 个这个侵入链表的头,一个用于 RequestIo 对象用于读取,另一个用于写入。通过这种方式,Stream 对象可以拥有这些RequestIo 对象的小池,并且不必在每次 i/o 操作时分配/解除分配它们,也不需要锁定,因为 2 个侵入性列表是分开的以读取IOCP 中可能同时发生在 2 个不同线程中的 2 种操作。

当我有类似流的对象(例如套接字或管道)时,我将只有 1 个RequestIo 用于读取(不需要多个)和一个用于写入,所以我基本上不需要锁定,因为在第一个socket.read() 分配了一个新的RequestIo,插入到链表中,并且将一遍又一遍地重用,直到套接字关闭并销毁,写入也是如此。

但是,没有类似流的对象(例如随机访问文件、udp 套接字)可以发出多个RequestIo 用于读取或写入。让我们只考虑一个 UDP 套接字,它可以发出 N 个待处理的 RequestIo 对象来读取数据报,或者一个随机访问文件,它可以发出多个 RequestIo 数据包来读取/写入文件的不同部分。

这里的事情变得复杂了。如果我有 RequestIo 对象的链接列表,我实际上必须遍历该列表并查看哪个 RequestIoNOT 挂起的,并使用该对象发出新的 i/o 操作。

说这样看起来很容易,但事实并非如此: 尽管我可以为RequestIo 设置一个标志,上面写着“待处理”,但问题并没有解决:该标志不应该是一个原子整数吗?因为该标志将被其他线程取消设置。当有多个 RequestIo 实例时,如何从链接列表中检索第一个可用的 RequestIo 呢?那不应该是一个联锁操作吗?并插入该链表?例如。当我分配一个新的RequestIo 数据包时,因为所有其他数据包都处于待处理状态。

我正在考虑的一个可能的解决方案是遍历这个链表并使用 CAS (CompareAndSwap) 指令检查 RequestIo 对象中的一个原子整数,如果 0 表示它没有挂起,则立即将其设置为 1,所以另一个线程将看到那个待处理,并将转到下一个RequestIo 对象。如果找不到任何RequestIo 对象,它会分配一个新对象,但在这里它应该锁定链表头...插入新分配的RequestIo 对象!

那么,什么是正确管理 N 个 OVERLAPPED(或在我的情况下为 RequestIo)对象池的最快和最有效的方法,而不会导致大量锁定,这会降低性能和多线程的目的IOCP?

【问题讨论】:

我不明白为什么列表中有待处理的 RequestIo 实例?您已通过 WSABlah 调用将它们发布到 IOCP 子系统。当 I/O 操作完成(或失败:)时,您会取回它们,那么为什么还要将它们保留在列表中? 此外,即使对于流服务,我通常也会尝试保持至少两个接收请求处于待处理状态,这样 IOCP 系统就不会经常陷入没有用户缓冲区可用于传入数据的情况。至于写入,多个 I/O 请求很常见,因为如果一个缓冲区可用于写入,您最好现在发送它,而不是等待一个完整的缓冲区数组表示,例如 html 文件/页面的 HTTP 包,在发送第一个块之前完整地组装。 我将它们放在一个链表中,这样我就不必每次都销毁和重新分配它们,即使关闭套接字并重新打开它,我也可以重用它们。我分配一次,然后重复使用它们。基本上对象Stream has_a N RequestIo 对象,在一个小链表池中。这样,当您必须执行一些 i/o 操作时,您可以从该链接列表中获取 RequestIo 并将其用于您的 i/o 操作。否则,使用 RequestIo 完成 i/o 操作后你会做什么?在读取的情况下,您可以使用相同的对象发出新的读取,但在写入的情况下呢? @MarcoPagliaricci 你太担心锁了。关键部分应该没问题,因为它有一个主自旋锁并且竞争窗口非常小 - 你只是推/弹出一个指针。 另外,我无法理解您为什么坚持跟踪那些正在使用的实例。就像 Harry 在下面建议的那样,当您完成其中包含的任何数据缓冲区时,只需将使用过的实例推回到完成处理程序线程中的空闲池中。与套接字相同,与数据缓冲区实例相同。 【参考方案1】:

保留一个仅包含未使用的RequestIo 对象的链表。您可以在需要时从列表头部弹出一个对象,并在完成后将每个对象推回列表中。

InitializeSListHead、InterlockedPushEntrySList 和 InterlockedPopEntrySList 函数提供了高效的多处理器安全链表实现。

【讨论】:

我已经想到了这一点,但在这里,我需要为这些列表设置锁定机制。我的意思是:lock(); get_first_available_RequestIo_for_writing(); remove_it_from_available_list(); unlock(); 等等 是的,你需要一把锁,但谁在乎呢?这将是一个关键部分,并且实际争用的机会(因此需要内核锁而不是 CS 自旋锁)是最小的。您所做的只是推送/弹出一个指向 RequstIo 实例的指针 - 不需要很长时间! 如果requestIo对象池用完,需要一个策略来处理。要么创建另一个 requestIO,从而增加池的大小,要么将池安排为阻塞队列,以便请求线程必须等待实例被释放。 你不需要锁。互锁的 push/pop 函数是原子的和多处理器安全的,即它们为您处理锁定。 Harry:哦,是的,有了你提供的那些API,我将使用无锁链表。谢谢。

以上是关于多线程环境中的扩展 OVERLAPPED 对象池:在何处以及如何有效地使用锁定的主要内容,如果未能解决你的问题,请参考以下文章

如何扩展和优化线程池?

线程池Executors.newFixedThreadPool

Dubbo之线程池设计

使用线程池优化多线程编程

Java多线程-12(线程管理中的线程池)

一起Talk Android吧(第三百七十一回:多线程之线程池扩展)