Task.Run 本质是 Task.Factory.StartNew 的简化封装
Task.Run 不是独立的底层机制,而是对
Task.Factory.StartNew的一层轻量包装,它固定使用
TaskCreationOptions.DenyChildAttach和默认的
TaskScheduler.Default(即线程池调度器),并只暴露最常用参数。如果你看到
Task.Run(() => DoWork()),它等价于:
Task.Factory.StartNew(() => DoWork(),
CancellationToken.None,
TaskCreationOptions.DenyChildAttach)
这意味着:你无法用
Task.Run指定自定义调度器(比如 UI 线程的
TaskScheduler.FromCurrentSynchronizationContext()),也不能启用子任务附加(
AttachedToParent)——这些必须用
Task.Factory.StartNew显式控制。
什么时候必须用 Task.Factory.StartNew
以下场景
Task.Run无能为力,只能选
Task.Factory.StartNew: 需要把子任务标记为
AttachedToParent,以便父任务等待所有子任务完成 要在 WinForms 或 WPF 中直接调度到 UI 线程(需传入
TaskScheduler.FromCurrentSynchronizationContext()) 要配合
CancellationToken做精细取消协作(
Task.Run虽支持传入 token,但不支持在调度前就响应取消,而
StartNew可以) 需要复用非默认的
TaskScheduler,比如自定义线程池或限流调度器
Task.Run 更安全,也更容易误用
Task.Run的设计目标是“简单且不易出错”,但它隐藏了调度细节,容易让人忽略执行上下文问题: 在 UI 线程调用
Task.Run后,回调里访问 UI 控件会抛
InvalidOperationException—— 因为它一定跑在线程池线程上 如果函数体里用了
await,记得返回
Task并用
Task.Run(async () => await ...),否则会同步执行前半段(常见坑)
Task.Run不支持传入带状态的对象(
object state参数),而
StartNew支持,这对某些旧式回调模式仍有用
性能差异几乎可以忽略
两者底层都走相同的任务队列和线程池路径,差别仅在于对象创建开销:
Task.Run少一个参数解析和选项合并过程,但这个差距在纳秒级,对任何真实业务无影响。别为了“性能”刻意换用
StartNew;选哪个取决于是否需要它提供的控制能力。
真正要注意的是:不要在热路径里高频创建大量短生命周期任务,无论用哪个 API,都会增加调度器压力。该用
ValueTask或同步逻辑的地方,别硬套
Task。
