多线程环境中的扩展 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
,我还有各种派生对象,例如 File
、TcpSocket
、UdpSocket
、Pipe
直接派生自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
对象的链接列表,我实际上必须遍历该列表并查看哪个 RequestIo
是 NOT 挂起的,并使用该对象发出新的 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 对象池:在何处以及如何有效地使用锁定的主要内容,如果未能解决你的问题,请参考以下文章