windows - I/O完成端口* LAST *称为回调,或: where it's safe to cleanup things

标签 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的类,并且
I/O完成时,IO完成端口线程将调用其回调。另外,您可以缓存
并重用存储在这些指针中的OVERLAPPED结构(由IoPacket派生)。

您可以通过自己分配一个Stream对象来使用此类,也可以只让其他类来分配该类
连接到达后(例如AcceptEx()或命名管道服务器)。流对象将
根据其回调实现自动开始执行I/O操作。

现在,在两种情况下,您都选择[1)自己分配流(客户端)或2)从其他对象(服务器)分配流]
会有一个普遍的问题:您不确切知道何时可以安全地从内存中释放Stream对象。即使对IoPacket对象使用原子引用计数器,也是如此。

让我们看一下原因:做I/O时,Stream对象指针将包含在IoPacket对象中,该对象依次由线程池中的其他线程处理和使用,线程池又使用该指针来调用回调[onRead() ,onWrite()等...]。您还可以使用“m_pending_reads/writes”变量来查看有多少未决读/写。因此,Stream对象指针将跨线程共享。

在这一点上,让我们考虑您不再需要该连接,并且您想要关闭并取消分配相关的Stream对象。

如何安全地做到这一点?

我的意思是,如果您只是这样做:
stream->close();
delete stream;

您将最终陷入崩溃,因为另一个线程可能仍在使用“流”指针。
安全的方法是这样的:
stream->close();
while ((stream->m_pending_reads != 0) && (stream->m_pending_writes != 0));
delete stream;

但这是完全无效的,因为这会阻塞执行路径g,阻塞等待其他线程
用“流”对象完成工作。 (此外,我想我还会添加一些 volatile 或屏障机制?)
最好的方法是异步执行此操作,另一个线程为该Stream对象完成LAST IoPacket,然后调用onClose(),依次为:
void onClose() { delete this; }

所以我的问题是:如何识别我正在处理(在任何线程中)给定句柄的LAST IoPacket(或OVERLAPPED),所以在我可以安全地调用onClose()例程后,该例程删除了Stream对象,该对象在turn删除用于执行I/O任务的缓存IoPackets。

现在,我使用这种方法:如果(为了阅读补全),“GetQueuedCompletionStatus()”返回ERROR_OPERATION_ABORTED [因此,它表示CloseHandle(m_handle); [已经被调用] OR bytes_read == 0 [这表示EOF]或(state!= Open)则表示它是最后一个未决的读取数据包,然后调用onClose();经过处理。写数据包将忽略onClose()。

我不确定这是正确的方法,因为即使读取的数据包是
在发生I/O事件之前,该事件始终处于挂起状态,可能发生在计划处理另一个写数据包的另一个线程之前的某个线程调用onClose()的情况。

那些还具有其他IoPackets的Stream对象派生类(例如TcpClient)又如何呢?例如一个用于ConnectEx()。

一个可能的解决方案是让AtomicInt m_total_pending_requests; <-,并且当该原子整数达到0时,完成线程调用onClose();。

但是,这最后一个解决方案无效吗?为什么我要使用原子整数来完成诸如关闭连接并取消分配其结构之类的基本操作?

也许我对那些带有Stream对象的IOCP的设计完全走错了路,但是我想这是抽象用于处理HANDLE的非常常用的设计,所以我想听听IOCP专家的意见。
我是否必须更改所有内容,否则Stream对象的这种设计可以非常可靠,安全且最重要,快速地工作?我之所以这样问,是因为完成诸如close + free内存之类的基本操作很复杂。

最佳答案

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

您只需执行任何会生成IOCP补全的操作,就可以增加引用计数,并在处理完补全后将其递减。尽管有一些未完成的操作,但您的引用大于1,并且handle对象有效。完成所有操作后,您的引用将降为0,并且该句柄可以自行删除。

我对数据缓冲区也做同样的事情,因为我的代码允许每个连接进行多个并发的send/recv操作,因此不能将重叠的结构存储在句柄中。

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

关于windows - I/O完成端口* LAST *称为回调,或: where it's safe to cleanup things,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21907157/

相关文章:

windows - 为什么 appcmd.exe 在批处理文件中执行时表现不同?

python - 与 windows 相比,ubuntu 中 python 程序的运行时间非常小。为什么?

java - 如何在Java中停止线程

C11线程编程

java - 在将大文件发送到多个客户端的同时提高性能

node.js - 使用nodejs实时播放HLS文件

C++ 重载 << 用作流的类对象的运算符

java - 从Java进行Exchange Web服务身份验证

windows - 将子文件夹中的文件 move 到文件夹并重命名 windows 提示

Java多线程同步方法有两个线程