线程池

线程池是指工作线程的集合,而这些线程可代表应用程序来高效执行异步回调。 线程池主要用于减少应用程序线程数,并提供对工作线程的管理。 应用程序可对工作项进行排队、将工作与可等待句柄相关联、根据计时器自动排队,并与 I/O 绑定。

线程池体系结构

以下应用程序可从使用线程池中受益:

  • 高度并行且可异步调度大量小型工作项(例如,分布式索引搜索或网络 I/O)的应用程序。
  • 可创建和销毁每次仅运行一小段时间的大量线程的应用程序。 使用线程池可降低线程管理的复杂性,以及线程创建和销毁所涉及的开销。
  • 可在后台处理以及并行处理(例如,加载多个选项卡)独立工作项的应用程序。
  • 必须对内核对象执行排他等待或对某一对象的传入事件进行阻止的应用程序。 使用线程池可降低线程管理的复杂性,并通过减少上下文切换的次数来提高性能。
  • 可创建自定义服务线程以等待事件的应用程序。

原始线程池已在 Windows Vista 中彻底重新架构。 现已改进新的线程池,因为它可提供单个工作线程类型(同时支持 I/O 和非 I/O),不使用计时器线程、可提供单个计时器队列,并可提供专用持久线程。 此外,它还提供清理组、更高的性能、独立安排的多个池(每进程),以及新的线程池 API。

线程池体系结构包括以下内容:

  • 执行回调函数的工作线程
  • 等待多个等待句柄的服务线程
  • 工作队列
  • 每个进程的默认线程池
  • 用于管理工作线程的工作工厂

最佳方案

较之原始线程池 API,新的线程池 API 提供更高的灵活性和控制力。 但是,其中也有一些细微但重要的区别。 在原始 API 中,等待重置会自动执行;但在新 API 中,每次均须显式重置等待。 原始 API 可自动处理模拟,从而将调用进程的安全上下文传输到线程。 而对于新的 API,应用程序必须显式设置安全上下文。

使用线程池时的最佳做法如下:

  • 某一进程的线程会共享线程池。 单个工作线程可一次执行多个回调函数。 这些工作线程由线程池进行管理。 因此,请勿通过对线程调用 TerminateThread 或从回调函数调用 ExitThread 来终止线程池中的线程。

  • I/O 请求可在线程池内的任意线程中运行。 取消线程池线程的 I/O 需进行同步,因为取消函数可能会在与处理该 I/O 请求的不同线程中运行,而这可能会导致某一未知操作被取消。 为避免出现此情况,请始终提供在调用 CancelIoEx 以进行异步 I/O 时所启动 I/O 请求的 OVERLAPPED 结构,或是使用自己的同步来确保在调用 CancelSynchronousIoCancelIoEx 函数之前,目标线程中无法启动任何其他 I/O。

  • 清理在从此函数返回之前在回调函数内创建的所有资源。 其中包括 TLS、安全上下文、线程优先级和 COM 注册。 此外,回调函数还须在返回之前还原线程状态。

  • 将等待句柄及其关联的对象保持为活动状态,直到线程池表明它已处理完该句柄。

  • 对等待长时间操作的所有线程(例如,I/O 刷新或资源清理)进行标记,以便线程池可分配新线程,而不是等待此线程。

  • 卸载使用该线程池的 DLL 之前,请取消所有工作项、I/O、等待操作和计时器,并等待回调执行完成。

  • 通过消除工作项与回调之间的依赖关系、确保回调不等待自身完成以及保留线程优先级来避免发生死锁。

  • 请勿在某一进程中借助使用默认线程池的其他组件对过大量项目进行过快排队。 每个进程均有一个默认线程池,包括 Svchost.exe。 默认情况下,每个线程池最多有 500 个工作线程。 当处于就绪/正在运行状态的工作线程数必须小于处理器数时,线程池会尝试创建更多工作线程。

  • 请避免使用 COM 单线程单元模型,因为它与线程池不兼容。 STA 会创建可能会影响该线程的下一工作项的线程状态。 STA 通常生存期较长且具有线程相关性,而这与线程池恰好相反。

  • 创建新的线程池来控制线程优先级和隔离、创建自定义特征,并试图提高响应能力。 但是,新增的线程池需要更多系统资源(线程、内核内存)。 池过多会增大 CPU 争用的可能性。

  • 如果可能,请使用可等待的对象而不是基于 APC 的机制来向线程池线程发出信号。 不同于其他信号机制,APC 不太适用于线程池线程,因为系统会控制线程池线程的生存期,因此在传送通知之前可能会终止某一线程。

  • 使用线程池调试器扩展 !tp。 此命令的用法如下:

    • pool 地址 标志
    • obj 地址 标志
    • tqueue 地址 标志
    • waiter 地址
    • worker 地址

    对于 pool、waiter、和 worker,如果地址为零,该命令则会转储所有对象。 对于 waiter 和 worker,省略地址则会转储当前线程。 现已定义以下标志:0x1(单行输出)、0x2(转储成员)和 0x4(转储池工作队列)。

线程池 API

使用线程池函数