介绍
一般应用程序不直接使用 System.Threading 中的 Thread 类。如果您在某个项目中看到它,则表明您正在维护遗留的代码。众所周知,.NET 1.0 引入了 Thread API,但使用它并不那么容易。最好不要使用它,为什么呢?
1.Thread API 是底层 API,可让您直接管理一切。这个选项听起来不错,但对不熟悉的东西进行更多控制可能会最终令人沮丧。
2.掌握 Thread API 并非易事。它有很多背景历史,需要你深入研究操作系统的细节才能完全理解。嗯,听起来有点吓人 )
3.Thread API 迫使您从线程而不是任务的角度来思考。作为开发人员,您应该考虑如何让代码并发,而不是考虑如何创建线程。线程是一个物理概念,是一种实现细节。
4.线程创建是一项昂贵的操作。
5.从可理解性和可维护性的角度来看,使用线程 API 会增加额外的复杂性。
在 .NET 2.0 中,微软引入了比 Thread API 更好的 ThreadPool 类。主要改进是将我们从 Thread 概念中分离出来。您不需要从线程的角度来思考。作为开发人员,您应该从要并行化的任务的角度来思考。
ThreadPool 是线程的包装器。
为什么它是一个更好的选择?
1.通过 ThreadPool,你不再是线程的“所有者”。无需手动创建,无需复杂操作,也无需担心线程管理、死锁等问题。猜猜怎么着?最大的问题不是线程,而是你 😃
2.ThreadPool 管理何时以及为何创建线程。简单来说,假设我们有 5 个方法。您想以多线程模式运行它们。您需要 5 个方法来运行它们,但 ThreadPool 可能会使用 2 或 3 个线程来完成您的任务。限制取决于您在传统线程 API 中使用的线程数。
internal class Program
{
static void Main(string[] args)
{
new Thread(ComplexTask).Start();
Thread.Sleep(1000);
new Thread(ComplexTask).Start();
Thread.Sleep(1000);
new Thread(ComplexTask).Start();
Thread.Sleep(1000);
new Thread(ComplexTask).Start();
Thread.Sleep(1000);
Console.ReadLine();
}
static void ComplexTask()
{
Console.WriteLine($"Running complex task in Thread={Environment.CurrentManagedThreadId}");
//Thread.Sleep(40);
Console.WriteLine($"Finishing complex task in Thread={Environment.CurrentManagedThreadId}");
}
}
结果如下
每个操作都是一个线程,这不是一个优化的使用方式。
ThreadPool 将我们从 Thread 中分离出来,并提供了一个简单的 API 来进入多线程模式。让我们修改代码,并尝试理解直接使用 ThreadPool 而不是 Thread 的价值。
internal class Program
{
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem((x) => ComplexTask());
Thread.Sleep(1000);
ThreadPool.QueueUserWorkItem((x) => ComplexTask());
Thread.Sleep(1000);
ThreadPool.QueueUserWorkItem((x) => ComplexTask());
Thread.Sleep(1000);
ThreadPool.QueueUserWorkItem((x) => ComplexTask());
Thread.Sleep(1000);
Console.ReadLine();
}
static void ComplexTask()
{
Console.WriteLine($"Running complex task in Thread={Environment.CurrentManagedThreadId}");
//Thread.Sleep(40);
Console.WriteLine($"Finishing complex task in Thread={Environment.CurrentManagedThreadId}");
}
}
正如您所意识到的,ThreadPool 使用 Thread,但方式经过了优化。如果任何现有线程空闲,ThreadPool 将使用已存在的线程,而不是创建新线程。等一下…我们是否可以执行相同的操作?我的意思是,我们可以运行同一个线程两次吗?让我们试试。
您无法两次运行同一个线程,但 ThreadPool 却可以。ThreadPool 可以重用线程,当您要在多线程模式下运行代码块时,它有助于避免每次都创建线程。在经典 .NET 中,根据其版本和您的 CPU 架构,工作线程的容量会有所不同。
就我而言,我使用的是带有 x64 英特尔处理器的最新 .NET(.NET 8),并且拥有 32767 个工作线程和 1000 个完成后线程。
ThreadPool 通常以池中的 1 个线程开始。根据上下文,它可能会自动增加线程。您可以检查 ThreadPool 中的可用线程。
internal class Program
{
static void Main(string[] args)
{
ThreadCalculator();
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem((x) => ComplexTask());
}
Thread.Sleep(15);
ThreadCalculator();
Console.ReadLine();
}
static void ThreadCalculator()
{
ThreadPool.GetAvailableThreads(out int workerThreads, out int completionPortThreads);
Console.WriteLine($"worker threads = {workerThreads}, " +
$"and completion Port Threads = {completionPortThreads}");
}
static void ComplexTask()
{
Console.WriteLine($"Running complex task in Thread={Environment.CurrentManagedThreadId}");
//Thread.Sleep(40);
Console.WriteLine($"Finishing complex task in Thread={Environment.CurrentManagedThreadId}");
}
}
ThreadPool 中最常用的 API 当然是 QueueUserWorkItem。
我们需要介绍一下。ThreadPool.QueueUserWorkItem 是 C# 中的一种方法,它允许您安排任务在线程池中执行。以下是其功能的细分:
目的
- 安排任务异步运行,无需创建和管理单独的线程。
- 利用预先创建的线程池,提高性能和资源管理。
怎么运行的
提供一个代表您想要完成的工作的委托(例如 Action 或 WaitCallback)。或者,您可以传递一个包含委托要使用的数据的对象。ThreadPool.QueueUserWorkItem 将委托和数据(如果提供)添加到线程池的队列。
当池中的线程可用时,它会从队列中获取第一个项目并使用提供的数据执行委托。
好处或使用原因
- **性能提升:**重用线程避免了为每个任务创建和销毁线程的开销。
- **资源优化:**维持受控的线程数,防止系统过载。
- **简化的并发管理:**线程池处理调度并确保任务高效执行。
补充笔记
-
线程池可以根据工作负载动态调整线程数量。
-
如果所有线程都处于繁忙状态,则将任务排队,以确保不会丢弃任何任务。
-
ThreadPool.QueueUserWorkItem 具有重载,包括类型安全的通用版本和影响线程选择的选项。
-
好的,但是完成端口线程怎么样?
C# 中的 ThreadPool 类中的 finishPortThreads 概念可能不会像您期望的那样直接公开。
ThreadPool 类内部管理两种类型的线程。
- 工作线程:这些线程处理通过 QueueUserWorkItem 提交的通用任务。
- **I/O 完成端口线程(Completion Port Threads):**这些是针对处理异步 I/O 操作而优化的专门线程。
完成端口线程的用途
异步 I/O 操作通常涉及等待网络请求、文件访问或其他外部事件。完成端口线程使用称为 I/O 完成端口 (IOCP) 的系统机制高效地等待这些事件。
当 I/O 操作完成时,相应的完成端口线程会收到通知,然后它可以出队并处理已完成的任务。
有什么价值?
- 提高了异步 I/O 绑定任务的性能。
- 完成端口线程避免忙等待,减少等待I/O事件时的CPU使用率。
- 用于 I/O 操作的专用线程可防止工作线程被阻塞,从而提高整体响应能力。
重要笔记
您不能直接控制completionPortThreads。ThreadPool 类根据系统工作负载动态管理其数量。
ThreadPool.SetMinThreads 和 ThreadPool.GetAvailableThreads 等方法允许您间接影响工作线程和完成端口线程的最小数量,但是对每种类型没有单独的控制。
谁使用 C# 中的 ThreadPool?
ThreadPool 是 C# 中用于高效管理线程的基本机制。它提供了一个工作线程池,可供应用程序的各个部分重复使用,包括:
- **任务并行库 (TPL):**当您创建 Task 或 Task 对象时,TPL 通常默认安排它们在 ThreadPool 线程上运行。这样便可以并行执行任务,而无需明确管理线程。
- **异步编程:**使用 async 和 await 关键字等异步操作通常依赖于 ThreadPool 在后台执行实际工作,同时主线程保持响应。
- **并行编程:**并行 For 循环 (Parallel.For) 和 PLINQ (Parallel LINQ) 等库通常利用 ThreadPool 在多个线程之间分配工作项。
使用线程池的好处
- 减少线程创建开销:创建线程可能很昂贵。ThreadPool 通过预先创建线程池并根据需要重复使用它们来消除这种开销。
- 提高性能:通过有效地管理线程,ThreadPool 可以增强应用程序的响应能力和吞吐量,尤其是在处理并发任务时。
- 简化的线程管理: ThreadPool 处理线程的创建、销毁和空闲线程管理,使您摆脱这些复杂性。
何时考虑替代方案
- **长时间运行的操作:**如果您的工作项需要长时间运行(例如,几秒或更长时间),则使用专用线程或带有自定义线程创建选项的 TaskFactory.StartNew 方法可能更合适,以避免线程池饱和并影响整体应用程序性能。
- **专门的线程要求:**如果您的任务需要特定的线程优先,您可能需要创建具有所需设置的专用线程。
结论
ThreadPool 是 C# 中并发编程的宝贵工具。它提供了一种方便而有效的线程管理方法,尤其是对于短暂的、受 CPU 限制的任务。通过了解 ThreadPool 的工作原理及其适当的用例,您可以创建结构良好且性能良好的 C# 应用程序。