应自己实现线程池而非使用.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,或直接用 ParallelOptions.TaskScheduler 控制并发粒度。
真正容易被忽略的是:线程池不是万能加速器。如果任务本身是同步阻塞 IO(如 File.ReadAllText),开 100 个线程只会压垮磁盘;换成 await File.ReadAllTextAsync 再配合 Task.Run 包裹 CPU 密集操作,才合理。










