应自己实现线程池而非使用.NET内置ThreadPool,因其全局静态、不可配置上限、无法感知任务状态;需固定线程数、独立生命周期、自定义拒绝策略或调试可见性时,宜手写轻量级池,核心含任务队列、工作线程集与控制开关,推荐BackgroundService+BlockingCollection实现安全启停。
ThreadPool 而要自己实现?因为 .NET 内置的 ThreadPool 是全局、静态、不可配置线程数上限的(SetMaxThreads 影响整个进程),且无法感知任务排队/执行状态。如果你需要:固定线程数、独立生命周期、自定义拒绝策略、或调试时清晰看到每个线程在干什么,就得手写一个轻量级线程池。
一个最小可用线程池只需三部分:Task 队列、工作线程集合、控制开关。不推荐用 Thread 手动管理(易泄漏、难回收),改用 BackgroundService + BlockingCollection 更安全。
BlockingCollection 自带线程安全与阻塞取值,省去手动加锁while (!stoppingToken.IsCancellationRequested) 循环取任务Task.Run 启动固定数量的后台任务,不是 new Thread(...).Start()
关键在「关闭」逻辑。不能只停线程,必须让所有待处理任务有机会完成,同时阻止新任务入队。常见错误是调用 CompleteAdding() 后没等 IsCompleted 就 Dispose 队列。
Submit(Action) 方法,内部先检查 _queue.IsAddingCompleted,已关闭则抛 InvalidOperationException
StopAsync() 中先调用 _queue.CompleteAdding(),再 await Task.WhenAll(_workers)
_queue.TryTake(out var work, timeout: -1, cancellationToken),支持取消信号穿透public class SimpleThreadPool : IHostedService
{
private readonly BlockingCollection _queue = new();
private readonly List _workers = new();
private readonly int _workerCount;
public SimpleThreadPool(int workerCount = 4) => _workerCount = Math.Max(1, workerCount);
public Task StartAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < _workerCount; i++)
{
_workers.Add(Task.Run(() => WorkerLoop(cancellationToken), cancellationToken));
}
return Task.CompletedTask;
}
private void WorkerLoop(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
if (_queue.TryTake(out var work, -1, ct))
work();
}
}
public void Submit(Action action)
{
if (_queue.IsAddingCompleted)
throw new InvalidOperationException("Pool is stopping or stopped.");
_queue.Add(action);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_queue.CompleteAdding();
await Task.WhenAll(_workers).WaitAsync(cancellationToken);
_queue.Dispose();
}
}
ThreadPool 或 TaskScheduler?当你开始给线程加优先级、绑定 CPU 核心、做 IO 与计算任务分离调度,或者需要和 async/await 深度集成时,手写池就变成负优化。此时应转向 ConcurrentExclusiveSchedulerPair、自定义 TaskScheduler,或直接用 ParallelOpti 控制并发粒度。
ons.TaskScheduler
真正容易被忽略的是:线程池不是万能加速器。如果任务本身是同步阻塞 IO(如 File.ReadAllText),开 100 个线程只会压垮磁盘;换成 await File.ReadAllTextAsync 再配合 Task.Run 包裹 CPU 密集操作,才合理。