应使用 Task 而非 Thread 启动并发操作,因其由线程池调度、支持 async/await、异常可捕获且免手动管理生命周期;UI 中禁用 new Thread,推荐 Task.Run;需限流时配合 SemaphoreSlim;共享变量须加锁或用线程安全类型;async/await 不等于多线程,I/O 操作应优先用原生异步方法,CPU 密集型才用 Task.Run;并行执行用 Task.WhenAll;务必传递和响应 CancellationToken,及时处理异常。

用 Task 而不是 Thread 启动并发操作
直接 new Thread 已是过时做法,.NET 4.0+ 的标准路径是 Task。它由线程池调度、支持 async/await、能自然捕获异常,且避免手动管理生命周期。
常见错误:在 UI 线程里用 new Thread(() => { /* 耗时操作 */ }).Start() —— 不仅无法 await,异常还会静默丢失,UI 可能卡死。
- 推荐写法:
var task = Task.Run(() => DoHeavyWork());,后续可await task或task.Wait() - 若需控制并发数(比如限制最多 4 个并行任务),用
SemaphoreSlim包裹Task.Run调用 -
Task.Factory.StartNew有额外重载参数(如CancellationToken、调度选项),但多数场景Task.Run更安全、语义更清晰
共享变量必须加锁或换用线程安全类型
多个 Task 同时读写同一个 int、List<t></t> 或自定义对象字段,不出错纯属运气。竞态条件(race condition)会导致值突变、索引越界、数据丢失,且极难复现。
典型现象:counter++ 在 10 个任务中各执行 1000 次,最终 counter 却小于 10000;或 list.Add(x) 抛出 InvalidOperationException。
- 简单计数器优先用
Interlocked.Increment(ref counter),比lock开销更低 - 集合类改用
ConcurrentQueue<t></t>、ConcurrentDictionary<k></k>,它们内部已做细粒度同步 - 真要 lock,确保锁对象是私有静态/实例 readonly 字段,绝不用
lock(this)或lock(typeof(X))—— 容易引发死锁或意外干扰
async/await 不等于多线程,别误以为写了 await 就自动并行
await 本质是“让出当前线程,等 I/O 完成后继续”,它本身不创建新线程。很多开发者以为 await Task.Delay(1000) 或 await httpClient.GetAsync(url) 会占用线程池线程 —— 实际上它们几乎不占 CPU,底层靠操作系统 I/O 完成端口(Windows)或 epoll/kqueue(Linux/macOS)驱动。
错误用法:await Task.Run(() => Thread.Sleep(1000)) —— 纯属浪费线程池资源,应直接用 await Task.Delay(1000)。
- I/O 密集型操作(HTTP 请求、文件读写、数据库查询)优先用原生异步方法(如
ReadAsync、ExecuteScalarAsync),它们才是真正的异步 - CPU 密集型操作(图像处理、加密计算)才需要
Task.Run搬到后台线程,且注意别无节制地启动几十个,会拖垮线程池 - 想真正并行执行多个异步操作?用
Task.WhenAll(task1, task2, task3),不是串行写三个await
别忽略 CancellationToken 和异常传播
一个没被取消的长时间运行任务,可能让整个应用无法退出;一个未被处理的 Task 异常,会在 GC 时触发 UnobservedTaskException 事件(.NET 6+ 默认终止进程)。
常见疏漏:调用 Task.Run 后只 await 却不检查 IsFaulted,或根本没传 CancellationToken 给耗时方法。
- 所有可取消的长时间操作,接口应接受
CancellationToken参数,并在内部定期调用token.ThrowIfCancellationRequested() -
await一个可能失败的Task,异常会原样抛出;但若用.Wait()或.Result,异常会被包进AggregateException,需遍历.InnerExceptions - 若任务已开始但需中途放弃,务必确保资源(如文件句柄、数据库连接)能被正确释放 ——
using+try/finally或await using是底线
多线程真正的复杂点不在语法,而在状态共享的边界是否清晰、取消是否及时响应、异常是否总被看见。哪怕只加一行 lock 或一个 CancellationToken,漏掉就可能让问题延后几小时甚至几天才暴露。


















