I / O完成端口* LAST *称为回调,或:清除事物的安全性

时间:2014-02-20 11:59:35

标签: windows multithreading sockets atomic iocp

我想这个论点很重要,值得一些空间。

让我们考虑一下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又删除用于执行i / o操作的缓存IoPackets。

现在我使用这个方法:if(用于读取完成)`GetQueuedCompletionStatus()'返回ERROR_OPERATION_ABORTED [所以它意味着CloseHandle(m_handle);已被调用] OR bytes_read == 0 [表示EOF]或者if(state!= Open)那么这意味着它是最后一个读取数据包挂起,然后调用onClose();经过处理后。写数据包将忽略onClose()。

我不确定这是正确的方法,因为即使读取数据包是那些 在发生i / o事件之前总是挂起,可能会发生onClose()将被某个线程调度调用之前另一个愿意处理最后一个写入数据包的线程。

那些还有其他IoPackets的Stream对象衍生物(例如TcpClient)呢?例如一个用于ConnectEx()。

可能的解决方案是使用AtomicInt m_total_pending_requests; < - 当此原子整数达到0时,完成线程调用onClose();

但是,这最后的解决方案是无效的吗?为什么我使用原子整数做一个基本的事情,如关闭连接和解除分配它的结构?

也许我完全错误地设计了具有这些Stream对象的IOCP,但我想这是一个非常常见的用于抽象HANDLEs的设计,所以我想听听IOCP大师的一些意见。 我是否必须改变一切,或者Stream对象的设计可以非常稳固,安全,最重要,快速?我问这个问题是因为做了像关闭+自由记忆这样的基本事情的复杂性。

1 个答案:

答案 0 :(得分:3)

引用计数解决了这个问题。

当您执行任何可生成IOCP完成的操作时,您只需递增引用计数,并在完成处理完成后递减它。虽然有一些未完成的操作,但您的参考资料是> 1,句柄对象有效。完成所有操作后,您的引用将降为0并且句柄可以自行删除。

我对数据缓冲区做同样的事情,因为我的代码允许每个连接进行多个并发发送/接收操作,因此在句柄本身中存储重叠结构不是一种选择。

我有一些代码可以演示上述内容,您可以从here下载。