让我们考虑一下C/C++中最常见的I / O完成端口设计,
有一个抽象HANDLE的结构(或类)及其一些属性,如下所示:
class Stream { enum { Open = 1,Closed = 0 }; // Dtor virtual ~Stream() { if (m_read_packet != 0) delete_packet(m_read_packet); // the same for write packet } // Functions: bool read(...) { if (m_read_packet != 0) m_read_packet = allocate_packet(); ReadFile(m_handle ...); } bool write(...); bool close() { m_state = Closed; if (!CloseHandle(m_handle)) return false; else return true; } // Callbacks: virtual void onRead(...); virtual void onWrite(...); virtual void onEof(); virtual void onClose(); // Other stuff .... // Properties: HANDLE m_handle; int m_state; int m_pending_reads; int m_pending_writes; IoPacket * m_read_packet; IoPacket * m_write_packet; };
这非常简单,你有这个抽象HANDLE的类,而且
IO完成端口线程在i / o完成时调用其回调.另外,你可以缓存
并重用存储在这些指针中的OVERLAPPED结构(由IoPacket派生).
您可以通过自己分配Stream对象来使用此类,或者仅通过让其他类进行分配来使用此类
它,在连接到达后(例如AcceptEx()或命名管道服务器). Stream对象会
根据其回调实现自动开始执行i / o操作.
现在,在这两种情况下,你选择[1]自己分配流(客户端)或2)从其他对象(服务器)]
会有一个常见的问题:您不确切地知道何时可以安全地从内存中释放Stream对象.即使您对IoPacket对象使用原子引用计数器也是如此.
让我们看看为什么:在执行i / o时,Stream对象指针将包含在IoPacket对象中,而IoPacket对象又由线程池中的其他线程处理和使用,而线程池中的其他线程又使用该指针来调用回调[onRead(),onWrite()等……].您还有`m_pending_reads / writes’变量来查看有多少挂起的读/写.因此,Stream对象指针将在线程之间共享.
此时,让我们考虑您不再需要该连接,并且您希望关闭并释放相关的Stream对象.
如何安全地做到这一点?
我的意思是,如果你只是这样做:
stream->close(); delete stream;
你最终会遇到一些崩溃,因为另一个线程可能仍在使用’stream’指针.
安全的方式是:
stream->close(); while ((stream->m_pending_reads != 0) && (stream->m_pending_writes != 0)); delete stream;
但这完全是无效的,因为这会阻塞执行路径,阻塞等待其他线程
用’stream’对象完成他们的工作. (另外我想我会添加一些挥发性或屏障机制?)
最好的方法是异步执行,另一个线程完成该Stream对象的LAST IoPacket,并调用onClose(),而onClose()依次为:
void onClose() { delete this; }
所以我的问题是:如何识别我正在处理(在任何线程中)给定句柄的LAST IoPacket(或OVERLAPPED),所以在我可以安全地调用onClose()例程之后删除Stream对象,其中dtor in转而删除缓存的IoPackets,用于执行i / o操作.
现在我使用这个方法:if(用于读取完成)`GetQueuedCompletionStatus()’返回ERROR_OPERATION_ABORTED [所以它意味着CloseHandle(m_handle);已被调用] OR bytes_read == 0 [表示EOF]或者if(state!= Open)那么这意味着它是最后一个读取数据包挂起,然后调用onClose();经过处理后.写数据包将忽略onClose().
我不确定这是正确的方法,因为即使读取数据包是那些
在I / O事件发生之前总是挂起,可能会发生onClose()将被另一个愿意处理最后一个写入数据包的线程之前调度的线程调用.
那些Stream对象衍生物(例如TcpClient)还有其他IoPackets呢?例如一个用于ConnectEx().
一个可能的解决方案是使用AtomicInt m_total_pending_requests; < - 当这个原子整数达到0时,完成线程调用onClose(); 但是,这最后的解决方案是无效的吗?为什么我使用原子整数来做一个基本的事情,如关闭连接和解除分配它的结构? 也许我完全没有使用那些Stream对象设计IOCPs,但我想这是一个非常常用的抽象HANDLEs设计,所以我想听听IOCP大师的一些意见.
我是否必须改变一切,或者Stream对象的设计可以非常稳固,安全,最重要,快速?我问这个问题是因为做了像关闭自由记忆这样的基本事情的复杂性.
解决方法
当您执行任何将生成IOCP完成的任何内容时,您只需递增引用计数,并在完成处理完成后递减它.虽然有一些未完成的操作,但您的参考资料是> 1,句柄对象有效.完成所有操作后,您的引用将降为0,并且句柄可以自行删除.
我对数据缓冲区做同样的事情,因为我的代码允许每个连接进行多个并发发送/ recv操作,因此在句柄本身中存储重叠结构不是一种选择.
我有一些代码演示上面的内容,你可以从here下载它.