在使用Windows IOCP时缓存重叠结构体

3
我正在使用Windows中的I/O完成端口,我有一个叫做"Stream"的对象,它类似于抽象HANDLE(因此它可以是套接字、文件等)。
当我调用Stream::read()或Stream::write()(如在文件情况下的ReadFile()/WriteFile()和在套接字情况下的WSARecv()/WSASend())时,我会分配一个新的OVERLAPPED结构,以便创建一个挂起的I/O请求,该请求将由其他线程在IOCP循环中完成。
然后,当OVERLAPPED结构由IOCP循环完成时,它将在那里被销毁。如果是这种情况,Stream::read()或Stream::write()将再次从IOCP循环调用,它们将实例化新的OVERLAPPED结构,并且会一直进行下去。
这很好用。但现在我想通过添加OVERLAPPED对象的缓存来改进它:当我的Stream对象执行大量读取或写入时,缓存OVERLAPPED结构是绝对有意义的。
但现在出现了一个问题:当我取消分配Stream对象时,我必须取消分配缓存的OVERLAPPED结构,但我怎么知道它们已经完成还是仍在挂起,而某个IOCP循环最终会完成它们呢?因此,这里需要一个原子引用计数,但现在问题是,如果我使用原子引用计数,我必须为每个读取或写入操作增加该引用计数,并在完成OVERLAPPED结构的IOCP循环或Stream删除时减少,这在服务器上是大量的操作,因此我最终会大量的增加/减少原子计数器。
这会非常负面地影响多线程的并发性吗?这是我唯一担心的事情,阻止我将此原子引用计数器放在每个OVERLAPPED结构中。
我的担忧是无基础的吗?
我认为这是一个重要的话题,值得指出,在SO上提问,以了解其他人对此的看法和使用IOCP缓存OVERLAPPED结构的方法是值得的。如果可能的话,我希望找到一个聪明的解决方案,而不使用原子引用计数器。

当操作需要毫秒级别的时间时,你却为纳秒级别的时间而苦恼。这完全符合“万恶之源”的格言。 - Hans Passant
3
当“OVERLAPPED”结构由IOCP循环完成后,它将在那里被销毁。- 你为什么要销毁它?它应该是流类的成员,或者属于某个其他的“IOCP缓冲区”类,在WSA调用中被缓存和使用,在IOCP完成之前将其数据应用于流,并在释放回池之前被释放。 - Martin James
Martin,是的,这正是我想要实现的,我想知道原子引用计数器是否可以用于那些“IOCPbuffer”结构。Hans,问题不在于纳秒或毫秒,而在于锁定,一堆LOCK指令会影响线程并发性,因为当一个内存区域(例如原子计数器)被原子地读取/写入时,其他核心必须等待原子事务的完成。 - Marco Pagliaricci
1
据我所知,您不需要任何原子计数器。如果您正在缓存/池化这些实例,则永远不会销毁它们,只需重新放入池中即可。池本身需要一个锁,但处理锁的时间肯定会被避免持续创建/销毁类实例所淹没。 - Martin James
哦...等一下,你是要将同一个缓冲区发送给多个客户端吗?这就是你需要refCount的原因吗? - Martin James
不,如果我分配一个新的I/O数据包,即IOCPbuffer结构,我将在Stream对象内保留其指针,然后调用ReadFile或WSARecv并获得ERROR_IO_PENDING:数据包挂起。现在:如果我调用Stream :: close(),它将调用CloseHandle(handle);另一个线程将完成挂起的读取,但问题是:我必须在哪里释放数据包?在IOCP循环中还是在Stream的dtor中,因为Stream对象具有该数据包的指针,因此共享其所有权?原子计数器可以解决这个线程安全问题 - Marco Pagliaricci
1个回答

3
假设您将数据缓冲区与OVERLAPPED结构一起绑定作为“每个操作”数据对象,那么池化它们以避免过多的分配/释放和堆片段化是一个好主意。
如果您仅将此对象用于I/O操作,则不需要引用计数,只需从池中拉取一个,在IOCP完成处理程序中使用WSASend / WSARecv执行操作,然后在使用完它后将其释放到池中。
但是,如果您想要更复杂一些,并允许其他代码传递这些缓冲区,则可以考虑引用计数是否能使它更加容易。我在我的当前框架中进行了这样的操作,它允许我具有网络方面的通用代码,然后将读取完成时的数据缓冲区传递给客户端代码,他们可以对其进行任何操作,并在完成后将其释放回池中。目前使用引用计数,但我正在移除它以进行微小的性能调整。引用计数仍然存在,但在大多数情况下,它只会从0- > 1,然后再次变为0,而不是在我的框架各个层中进行操作(这是通过使用智能指针将缓冲区的所有权传递到用户代码来完成的)。
在大多数情况下,我认为引用计数不太可能是您最昂贵的操作(即使在使用多个节点的情况下,在此情况下,您的缓冲区正在被多个节点使用)。更有可能的是将这些东西放回池中所涉及的锁定将成为您的瓶颈;我已经解决了这个问题,所以正在向更高的水果转移 ;)
您还谈到了您的“每个连接”对象并在那里本地缓存您的“每个操作”数据(在将它们推回分配器之前),尽管“每个操作”数据不严格需要引用计数,但是“每个连接”数据至少需要原子可修改的“进行中的操作数”计数,以便您可以知道何时可以释放它。同样,由于我的框架设计,这已成为常规引用计数,客户端代码可以持有引用,以及活动I/O操作。我还没有找到一种通用框架的解决此计数需求的方法。

谢谢Len。我这里有一个通用框架,所以我不需要向用户公开io请求数据包,而是想要隐藏它们。采用(A)从池中获取io请求数据包和(B)从iocp处理程序线程将其放回池中的常规设计确实会带来新的瓶颈:池的锁定。我的目标是保持相同的io请求数据包,直到连接的生命周期结束,也就是Stream对象的生命周期结束。现在我唯一关心的是何时释放io请求数据包,在Stream的dtor内部还是在iocp处理程序内部? - Marco Pagliaricci
如果数据包是挂起的,我将在iocp处理程序上执行它,如果它不再挂起,并且不再使用,我必须从Stream的dtor中释放它,否则它将泄漏一个数据包。现在的问题是,我不知道是否需要一些原子计数器(甚至是布尔值),它可以告诉我原子地数据包不再挂起,没有其他线程拥有它,因此我可以从Stream的dtor中删除它。有人说我在这里不需要原子性,但这是真的吗?我有点怀疑。 - Marco Pagliaricci
假设您在所有操作完成之前从未释放流,则不需要在每个操作数据中使用计数器,但是您可能需要在流中使用计数器来跟踪正在进行的操作。 - Len Holgate
我确实直到所有待处理操作完成之前都不会释放流。由于每次只执行1个操作,因此我有2个标志:待读取和待写入,因此当我有00 ==无待处理操作,11 ==同时等待写入和读取,01 ==等待读取等等。在调用ReadFile()之前设置标志,并在iocp处理程序中关闭它。在这里,我不需要原子标志或原子计数器,因为我只执行1个操作,对吗? - Marco Pagliaricci

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接