【问题标题】:Does .NET event-based asynchronous pattern for I/O operation blocked threads in underlying?.NET 基于事件的 I/O 操作异步模式是否阻塞了底层线程?
【发布时间】:2014-01-23 12:07:54
【问题描述】:

在典型的 .NET 世界中,我们对大多数 I/O 操作使用基于事件的异步模式(Event Handler),更具体地说,据我所知,引入了 I/O 完成端口以提高调度线程的效率,就像线程池一样,因此我们不需要手动维护(初始化和销毁​​)线程来处理大量的 I/O 响应。

同时,我很自然地认为等待 I/O 响应不需要因为硬件中断而阻塞现代 Windows 系统中的任何线程,直到我在最近的项目中看到了一些 C++ 代码,甚至在 web.xml 中看到了一些示例代码。

我没有任何 C++ 经验

第一个代码是关于串口监听的,伪C++代码(我用C#风格输入)是这样的:

    // loop checking the status
    while(serialPort.Buffer.Count==0)
    {
          Thread.Sleep(100);
    }  

    byte[] data = serialPort.Buffer;
    // processing the actual data...

第二段代码是关于C++中I/O完成端口的使用:

    while (::GetQueuedCompletionStatus(port,
                                       &bytesCopied,
                                       &completionKey,
                                       &overlapped,
                                       INFINITE))
    {
        if (0 == bytesCopied && 0 == completionKey && 0 == overlapped)
        {
            break;
        }
        else
        {
            // Process completion packet
        }
    }

显然,它们都阻塞了线程。

所以我的问题是:

  1. 为什么那些代码没有选择基于事件的无线程阻塞方式?

  2. 如果.NET底层使用第二个示例的代码,那么在做I/O操作时实际上有线程阻塞?

  3. (可能有点跑题了)当前一个回调仍在执行时,.NET I/O 操作回调是否允许同时重新进入?(根据我有限的测试,答案是否定的)以及为什么?

【问题讨论】:

    标签: c# c++ multithreading asynchronous threadpool


    【解决方案1】:

    首先,阻塞本身并不是坏事。 Windows 应用程序中的“主”GUI 线程触发其“OnClick”等事件,以响应从 Windows 消息队列(一个阻塞的生产者-消费者队列)接收到的消息。当没有收到消息时,线程阻塞在队列中。与大多数基于“非阻塞” select() 的服务器相同 - select 是一个阻塞调用,(尽管可以通过设置低/零超时来进行轮询 - 一个糟糕的设计)。

    1) 异步设计本质上更复杂。每个套接字的上下文数据(例如缓冲区)不能在基于堆栈的自动变量中维护,并且必须通过维护上下文对象的全局容器(必须通过事件中的套接字句柄查找)来跨事件维护当它们被触发时),或者通过发出带有 I/O 请求的上下文对象并从事件中的回调参数中检索它们。异步设计应该是完全异步的——如果可能的话,必须避免调用任何可能长时间阻塞的东西。在这方面,对不透明的外部库、数据库查询等的调用可能会很麻烦,会阻塞所谓的异步线程并阻止它响应事件。

    第一个代码 sn-p 太可怕了,我很难找到任何理由。 sleep() 循环轮询在响应输入时具有内置的平均 50 毫秒延迟。当存在更好的同步和异步解决方案时,简直是超级蹩脚。专用的读取线程、排队的 APC、(完成例程)和 IOCP 都可用于串行端口。

    第二个代码-sn-p 实际上是基于事件的异步。通过让处理程序线程使用完成消息返回的参数调用事件处理程序,您可以使它看起来更加“基于事件”。

    IOCP 是 Windows 首选的高性能 I/O 系统。它可以处理多种类型的 I/O 操作,并且它基于线程池的处理程序可以承受偶尔的阻塞或冗长的操作,而不会阻止进一步 I/O 完成的处理。通过调用传递用户缓冲区允许驱动程序直接在内核空间中加载它们并删除一层复制。它没有做的是避免在异步调用中维护上下文的需要。

    每个客户端同步线程通常用于可伸缩性要求被简单的内联代码和此类设计中固有的阻塞调用免疫的情况所淹没。处理串行通信不再是可扩展至数千个端口的问题。

    2) IOCP 处理程序线程在等待完成消息时会阻塞,当然。如果没有什么可做的,线程应该阻塞:)

    3) 他们应该这样做。添加额外的信号层以确保回调被串行处理会涉及更多开销,并在回调中添加任何类型的阻塞,从而阻止来自不需要阻塞的其他 IOCP 处理程序线程的其他回调的处理.由于上下文作为参数传入,IOCP 驱动的回调没有内在要求以串行方式运行。回调处理程序中的代码可以只是以状态机的方式对传递的信息进行操作。

    也就是说,如果 MS .NET 确实提供了信号/队列来强制串行、不可重入回调,我不会感到惊讶。经验不足的开发者。经常在多线程回调中做他们不应该做的事情,例如。在没有任何锁定的情况下访问全局/持久状态,或直接访问线程绑定的 GUI 控件。通过将调用包装到 Windows 消息中或以其他方式对调用进行序列化,以牺牲性能为代价消除了这种风险。

    【讨论】:

      【解决方案2】:
      1. 可能是因为异步编程很难。
      2. .Net,在 I/O 方法上,尽可能公开同步操作和异步操作。例如,您有TcpClient.ConnectTcpClient.ConnectAsync/TcpClient.BeginConnect。以“Begin”开头或以“Async”结尾的任何东西至少应该是异步的,这意味着没有阻塞的线程。 TcpClient.Connect 是阻塞的,因此可扩展性较差。
      3. 我不太确定,但我认为他们可以。问题是你为什么要这样做?以及如何将回调与其调用相匹配?

      【讨论】:

        猜你喜欢
        • 2018-04-06
        • 2017-02-21
        • 1970-01-01
        • 1970-01-01
        • 2019-09-24
        • 2017-12-20
        • 2011-11-14
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多