结论:新项目禁用 Thread,优先用 Task/async-await;Thread 仅适用于需独占 OS 线程、精确控制线程属性或维护旧框架的特殊场景,因其开销大、无内置取消/异常传播/返回值支持,且易引发竞态、静默崩溃等问题。

直接说结论:别用 Thread 写新项目,除非你明确需要控制线程生命周期或兼容旧框架;日常并发任务优先用 Task 或 async/await。
为什么 Thread 现在基本只适合教学和特殊场景
它太底层了——每个 Thread 实例都会绑定一个操作系统线程,创建开销大、调度成本高,且无法复用。现代 .NET(.NET 6+)默认线程池已高度优化,Task 默认就在池里跑,启动快、内存省、还自带取消、异常传播、延续(ContinueWith)等能力。Thread 不提供这些,全得自己手写。
常见误用现象:Thread.Start() 启动后主线程立刻退出,子线程被强制终止(尤其控制台程序没加 thread.Join() 或 Console.ReadKey());或者反复 new Thread(...).Start() 导致线程数爆炸,CPU 被拖垮。
适用场景仅限于:
- 必须独占一个 OS 线程(如某些 COM 组件或 Win32 API 要求 STA 模式)
- 需要精确控制线程优先级、前台/后台属性(
IsBackground = false阻止进程退出) - 维护老代码(.NET Framework 2.0–3.5 时代遗留系统)
Thread 创建时最容易漏掉的两个设置
不设 IsBackground 和不处理未捕获异常,是新手踩坑最密集的地方。
IsBackground = true 是默认值,但很多人不知道:前台线程(false)会阻止进程退出,后台线程不会。如果你启了一个前台线程又没 Join 它,程序可能卡住;反之若误设为前台,又忘了等它结束,程序就提前退出,任务丢一半。
未捕获异常更危险:Thread 中抛出的异常不会传回主线程,也不会被全局异常处理器(如 AppDomain.UnhandledException)捕获(.NET Core+ 已移除该事件),而是直接终止线程并静默失败。必须手动包一层 try/catch:
var thread = new Thread(() =>
{
try
{
DoWork();
}
catch (Exception ex)
{
// 必须在这里记录日志,否则什么也看不到
Console.WriteLine($"Thread error: {ex}");
}
});
传参和获取返回值,Thread 原生不支持函数签名
Thread 构造函数只接受无参委托(ThreadStart)或单参委托(ParameterizedThreadStart),没法直接传多个参数,也不能返回值。
典型 workaround 是封装类:
class Worker
{
public int Input { get; set; }
public string Result { get; private set; }
public void Execute()
{
Result = $"Processed {Input}";
}
}
var w = new Worker { Input = 42 };
var t = new Thread(() => w.Execute());
t.Start();
t.Join(); // 等待完成
Console.WriteLine(w.Result); // 才能拿到结果
但注意:这要求你显式同步访问 w.Result,如果多个线程写同一个字段,还得加 lock。而 Task.Run(() => DoWork(42)) 直接返回 Task<string>,await 就能安全取值。
线程同步不是“加个 lock 就完事”
共享变量读写不加保护,Thread 下极易出现竞态——比如两个线程同时执行 counter++,最终结果可能少加一次。但 lock 只解决临界区互斥,不解决死锁、锁粒度、内存可见性等问题。
容易忽略的关键点:
-
lock对象必须是引用类型且生命周期稳定(不能是局部变量或装箱值类型) - 不要 lock(this)、lock(typeof(X)) 或 lock(字符串字面量),易引发意外锁竞争
- 涉及 I/O 或长时间操作,别把整个方法体包进
lock,会阻塞其他线程 - 多资源加锁必须严格按固定顺序(如先锁 A 再锁 B),否则大概率死锁
简单计数场景,优先用 Interlocked.Increment(ref counter) —— 它是原子操作,无锁,性能更高。
真正难的从来不是怎么启动线程,而是怎么让它安全地读写数据、怎么知道它什么时候做完、怎么在它挂掉时及时发现。这些事 Thread 全推给你自己扛,而 Task 和 Channel 这类高层抽象已经默默帮你兜住了大部分底。


















