结论:优先使用 Task.Run 而非 Thread;Thread 易致资源泄漏、上下文丢失、不可复用,且不支持 async/await 和 ConfigureAwait;I/O 密集型任务应直接 await,勿用 Task.Run 包装。

直接说结论:用 Task,别碰 Thread —— 除非你真在写线程池调度器或需要精确控制线程生命周期。
为什么 Thread 在现代 C# 中基本不该用
手动管理 Thread 容易出三类问题:资源泄漏(忘记 Join() 或没处理异常导致线程静默退出)、上下文丢失(比如 HttpContext、AsyncLocal<t></t> 在新线程里为空)、以及无法复用——每次 new Thread(...).Start() 都要分配内核线程,开销大且不可控。
常见错误现象:Thread 启动后主线程结束,整个进程退出(没设 IsBackground = false);或者捕获不到子线程抛出的异常,程序崩溃但没堆栈。
- 永远不要用
Thread.Start()去跑异步方法(async void或async Task),它不会等待await,会提前返回 -
Thread不支持ConfigureAwait(false),没法避免同步上下文切换开销 - 如果你只是想“同时做几件事”,
Thread是错的抽象层;你要的是并发执行能力,不是线程本身
Task.Run 是最常用也最安全的并行入口
Task.Run 把工作交给 ThreadPool,复用已有线程,自动处理异常传播和等待逻辑。它适合 CPU 密集型任务(如图像处理、加密计算)。
使用场景:从 UI 线程卸载耗时计算、批量数据转换、本地文件解析等不涉及 I/O 等待的纯计算。
- 参数差异:
Task.Run(() => ComputeHeavy())返回Task;Task.Run(() => ComputeHeavy()).Result会阻塞当前线程——千万别在 UI 或 ASP.NET Core 请求线程里这么干 - 想等多个任务完成?用
await Task.WhenAll(task1, task2, task3),不是task1.Wait()+task2.Wait() - 如果任务可能长时间运行(> 500ms),考虑加取消令牌:
Task.Run(() => ComputeHeavy(), cancellationToken)
var tasks = urls.Select(url => Task.Run(() => DownloadAndParse(url))); await Task.WhenAll(tasks); // 所有下载+解析并行执行,全部完成后才继续
什么时候必须用 Task.Factory.StartNew?
绝大多数情况不需要。只有两种真实需求会用到它:
- 你需要指定
TaskCreationOptions.LongRunning(告诉线程池:“这活儿会长时间占用线程,别塞进普通工作队列,给我单独起个线程”) - 你需要传入自定义的
TaskScheduler(比如 WPF 的DispatcherSynchronizationContext,或测试用的模拟调度器)
注意:Task.Factory.StartNew 默认不捕获同步上下文,而 Task.Run 会(在 UI 应用中这点很重要)。所以别为了“看起来更底层”就换用它。
容易踩的坑:Task.Factory.StartNew(() => { await SomeAsync(); }) 这种写法返回的是 Task<task></task>,不是你想要的 Task —— 正确写法是 Task.Factory.StartNew(async () => await SomeAsync()).Unwrap(),但更简单的方式是直接用 Task.Run。
I/O 密集型任务别用 Task.Run 包装 async 方法
这是高频误用。比如把 HttpClient.GetAsync 或 File.ReadAllBytesAsync 套进 Task.Run:
// ❌ 错误:无谓增加线程池压力 Task.Run(() => httpClient.GetAsync(url)); <p>// ✅ 正确:直接 await,I/O 由操作系统完成,不占线程 await httpClient.GetAsync(url);
原因:真正的异步 I/O 不需要线程“等着”,它靠完成端口(IOCP)通知。加一层 Task.Run 反而浪费一个线程去空等回调。
判断依据很简单:方法名带 Async 后缀、返回 Task 或 Task<t></t>、内部没明显 CPU 循环——那就让它自然 await,别包。
真正复杂的点在于混合场景:比如“下载 JSON(I/O)→ 解析成对象(CPU)→ 写入数据库(I/O)”。这时候该拆分:I/O 步骤直接 await,CPU 步骤用 Task.Run 卸载。没人能替你画这条界线,得看 profiling 数据——线程栈里卡在 ReadAsync 就是 I/O 瓶颈,卡在 JsonSerializer.Deserialize 就是 CPU 瓶颈。



















