【问题标题】:Simple description of worker and I/O threads in .NET.NET 中工作线程和 I/O 线程的简单描述
【发布时间】:2011-01-07 04:22:18
【问题描述】:

很难在 .NET 中找到对工作线程和 I/O 线程的详细而简单的描述

我对这个主题有什么清楚的(但在技术上可能并不精确):

  • 工作线程是应该使用 CPU 进行工作的线程;
  • I/O 线程(也称为“完成端口线程”)应该使用设备驱动程序来完成工作,并且基本上“什么都不做”,只监视非 CPU 操作的完成情况。

不清楚的地方:

  • 虽然 ThreadPool.GetAvailableThreads 方法返回两种类型的可用线程数,但似乎没有公共 API 来安排 I/O 线程的工作。在 .NET 中只能手动创建工作线程?
  • 似乎单个I/O线程可以监控多个I/O操作。这是真的吗?如果是这样,为什么 ThreadPool 默认有这么多可用的 I/O 线程?
  • 在某些文本中,我读到了该回调,在 I/O 操作完成后由 I/O 线程执行触发。这是真的吗?考虑到这个回调是CPU操作,这不是工作线程的工作吗?
  • 更具体地说——ASP.NET 异步页面是否使用 I/O 线程?将 I/O 工作切换到单独的线程而不是增加工作线程的最大数量究竟有什么性能优势?是因为单个 I/O 线程确实监控多个操作吗?还是 Windows 在使用 I/O 线程时会更有效地进行上下文切换?

【问题讨论】:

    标签: .net multithreading iocp


    【解决方案1】:

    .net/CLR 中的术语“工作线程”通常仅指除主线程之外的任何线程,它们代表生成线程的应用程序执行某些“工作”。 “工作”实际上可能意味着任何事情,包括等待一些 I/O 完成。 ThreadPool 保留了工作线程的缓存,因为创建线程的成本很高。

    .net/CLR 中的术语“I/O 线程”是指 ThreadPool 保留的线程,以便从“重叠”的 win32 调用(也称为“完成端口 I/O”)调度 NativeOverlapped 回调。 CLR 维护自己的 I/O 完成端口,并且可以将任何句柄绑定到它(通过 ThreadPool.BindHandle API)。此处示例:http://blogs.msdn.com/junfeng/archive/2008/12/01/threadpool-bindhandle.aspx。许多 .net API 在内部使用这种机制来接收 NativeOverlapped 回调,尽管典型的 .net 开发人员不会直接使用它。

    “工作线程”和“I/O 线程”之间实际上没有技术上的区别——它们都是普通线程。但是 CLR ThreadPool 为每个线程池保留单独的池,只是为了避免对工作线程的高需求耗尽所有可用于调度本机 I/O 回调的线程,从而可能导致死锁。 (想象一个使用所有 250 个工作线程的应用程序,每个工作线程都在等待一些 I/O 完成)。

    开发人员在处理 I/O 回调时确实需要小心,以确保 I/O 线程返回到 ThreadPool ——也就是说,I/O 回调代码应该做最少的工作服务回调,然后将线程的控制权返回给 CLR 线程池。如果需要更多工作,则应在工作线程上安排该工作。否则,应用程序可能会“劫持”CLR 的保留 I/O 完成线程池以用作普通工作线程,从而导致上述死锁情况。

    一些很好的参考资料供进一步阅读: win32 I/O 完成端口:http://msdn.microsoft.com/en-us/library/aa365198(VS.85).aspx 托管线程池:http://msdn.microsoft.com/en-us/library/0ka9477y.aspx BindHandle 示例:http://blogs.msdn.com/junfeng/archive/2008/12/01/threadpool-bindhandle.aspx

    【讨论】:

    • 因此,一些公开 APM 模式的 API(例如 - WebRequest)在内部使用 ThreadPool.BindHandle 方法。 BeginXXX 方法将接受用户回调委托,将工作交给某些设备驱动程序并保留 ThreadPool I/O 线程以“等待”完成端口以通知外部设备工作完成。当收到通知时,I/O 线程唤醒并运行用户回调委托代码(这应该很快,以便将 I/O 线程快速返回到池中)。
    • 所以工作线程通常运行工作,由用户代码安排。而 I/O 线程通常运行由框架代码调度的工作,它与外部设备交互,需要等待其工作完成并在这个“外部”工作完成时运行用户回调逻辑。这都是正确的理解吗?
    • 几乎,但是操作系统如何调度实际 I/O 对 CLR 来说是不透明的。 CLR 的 I/O 线程没有被捆绑等待完成;而是操作系统在工作完成时通过 I/O 完成端口向 CLR 发出警报。
    • 我知道这个问题很老了,但也许有些人仍然可以为我回答这个问题:如果我调用多个 BeginXXX 方法(例如多个客户端网络流 BeginRead),它们每个都会获得自己的 I/O线程或所有线程都在同一个 I/O 线程中处理?我问这个,因为我想创建一个有很多客户端的服务器,如果所有这些 BeginX 方法都有自己的线程,可能会导致问题,对此不确定。
    【解决方案2】:

    我将首先描述 NT 中的程序如何使用异步 I/O。

    您可能熟悉 Win32 API 函数 ReadFile(例如),它是 Native API 函数 NtReadFile 的封装。这个函数允许你用异步 I/O 做两件事:

    • 您可以创建一个事件对象并将其传递给 NtReadFile。然后在读取操作完成时发出此事件。
    • 您可以将异步过程调用 (APC) 函数传递给 NtReadFile。本质上,这意味着当读取操作完成时,该函数将排队到启动该操作的线程,并在线程执行alertable wait时执行。

    但是,当 I/O 操作完成时,还有第三种通知方式。您可以创建一个I/O 完成端口 对象并将文件句柄与之关联。每当在与 I/O 完成端口关联的文件上完成操作时,操作的结果(如 I/O 状态)都会排队到 I/O 完成端口。然后,您可以设置一个专用线程来从队列中删除结果并执行适当的任务,例如调用回调函数。这本质上就是“I/O 工作线程”。

    普通的“工作线程”非常相似;它不是从队列中删除 I/O 结果,而是从队列中删除工作项。您可以将工作项(QueueUserWorkItem)排队并让工作线程执行它们。这可以防止您每次想要异步执行任务时都必须生成一个线程。

    【讨论】:

      【解决方案3】:

      比我更有技能的人会跳进来帮忙。

      工作线程有很多状态,它们由处理器等进行调度,您可以控制它们所做的一切。

      IO 完成端口由操作系统提供,用于涉及很少共享状态的非常具体的任务,因此使用起来更快。 .Net 中的一个很好的例子是 WCF 框架。对 WCF 服务的每个“调用”实际上都由 IO 完成端口执行,因为它们启动速度最快,并且操作系统会为您处理它们。

      【讨论】:

        【解决方案4】:

        简单地说,工作线程的目的是执行短时间的工作,并在完成后将自己删除。回调可用于通知父进程它已完成或传回数据。

        一个 I/O 线程将连续执行相同的操作或一系列操作,直到被父进程停止。之所以这么称呼它,是因为它通常运行的设备驱动程序会持续监控设备端口。 I/O 线程通常会在希望与其他线程通信时创建事件。

        所有进程都作为线程运行。 您的应用程序作为线程运行。 任何线程都可能产生工作线程或 I/O 线程(正如您所说的那样)。

        在性能和所用线程的数量或类型之间总是有一个很好的平衡。一个进程处理过多的回调或事件将严重降低其性能,因为它在处理它们时会中断其主进程循环的次数。

        工作线程的示例是在用户交互后将数据添加到数据库中,或者执行长时间的数学计算或将数据写入文件。通过使用工作线程可以释放主应用程序,这对于 GUI 最有用,因为它不会在执行任务时冻结。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-09-27
          • 2011-10-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多