Appearance
C# 多线程编程与异步实践指南
在现代软件开发中,多线程是提高程序性能和响应能力的关键技术。本文由浅入深地整理了 C# 多线程的核心概念、委托与事件的深层联系、线程池的运作机制以及实际开发中的避坑指南。
1. 核心概念与基础
1.1 进程与线程
- 进程 (Process):操作系统分配资源的基本单位。进程之间相互独立,拥有私有的虚拟内存空间。
- 线程 (Thread):进程中的基本执行单元,共享进程资源(内存、文件句柄)。多线程允许程序在同一时间执行多个任务。
1.2 前台线程与后台线程
- 前台线程:只有所有的前台线程都结束,应用程序才能结束。默认创建的线程是前台线程。
- 后台线程:只要所有的前台线程结束,后台线程会自动结束。通过
thread.IsBackground = true设置。 - 注意:后台线程适合处理不重要的背景任务(如日志记录),重要数据保存务必使用前台线程或确保任务执行完再退出。
2. 线程池 (ThreadPool) 深度解析
线程池通过复用线程资源,减少了创建和销毁线程的巨大开销。
2.1 线程池的工作原理
线程池包含 工作线程 (Worker Threads) 和 IO 完成端口线程 (IO Threads)。
- 最小线程数 (MinThreads):系统启动时立即准备好的线程数量。在达到此数量前,任务会被立即处理。
- 最大线程数 (MaxThreads):线程池能容纳的最大线程上限。达到上限后,新任务将进入队列等待。
2.2 线程创建频率实验
根据实验观察,当任务数量超过最小线程数时,线程池创建新线程的速度并非瞬间爆发,而是以 “每秒不超过 2 个”(通常是 0.5s 或 1s 一个)的频率缓慢增加,直到达到最大线程数。
原则:不要在线程池中执行长时间阻塞的操作(如
Thread.Sleep(10000)),这会导致池内线程耗尽,后续任务排队严重。
2.3 线程同步与死锁
在限制了最大线程数的场景下,如果任务之间存在依赖(如任务 A 等待任务 B 的信号,但任务 B 因为达到线程池上限而无法启动),就会发生 死锁。
csharp
// 警告:不要阻塞线程池里的线程
ThreadPool.SetMaxThreads(5, 5);
ManualResetEvent waitHandle = new ManualResetEvent(false);
// 推入 10 个任务,如果前 5 个都在等待信号,第 10 个发送信号的任务将永远无法执行3. 委托与事件:异步编程的基石
委托(Delegate)是方法的类型化引用,它是实现异步调用和观察者模式的核心。
3.1 委托与 Observer 模式
委托使得方法可以作为参数传递,从而实现松耦合的 观察者模式 (Observer Pattern)。
代码示例:基于委托的烧水提醒器
csharp
public delegate void BoilHandler(int param);
public class Heater {
private int temperature;
public event BoilHandler BoilEvent; // 声明事件
public void BoilWater() {
for (int i = 0; i <= 100; i++) {
temperature = i;
if (temperature > 95) {
// 触发事件,通知所有订阅者
BoilEvent?.Invoke(temperature);
}
}
}
}
public class Alarm {
public void MakeAlert(int param) => Console.WriteLine($"警报:水快开了,当前{param}度");
}3.2 委托 (Delegate) 与 事件 (Event) 的区别
很多开发者混淆两者,本质上:
- 委托 是一个类型,类似于类。它可以被赋值,也可以直接在外部调用
delegate1(args)。 - 事件 是对委托的 封装。它在内部被编译为
private委托字段和两个public方法(add_EventName和remove_EventName)。 - 封装性:在类的外部,事件只能使用
+=和-=,禁止使用=赋值,也禁止直接调用BoilEvent(95),这保证了事件只能由发布者触发。
3.3 .NET 框架中的标准事件模型
为了保证通用性,.NET 建议遵循以下规范:
- 委托原型:
void (object sender, EventArgs e)。 - sender:指向发布者实例,方便订阅者回调。
- EventArgs:封装传递的数据。
csharp
public class BoiledEventArgs : EventArgs {
public readonly int Temperature;
public BoiledEventArgs(int temp) => Temperature = temp;
}
public delegate void BoiledEventHandler(object sender, BoiledEventArgs e);
public event BoiledEventHandler Boiled;3.3 异步调用 (BeginInvoke/EndInvoke)
通过委托的 BeginInvoke,可以从线程池中抓取线程执行耗时操作,而主线程继续执行。
csharp
public delegate int AddDelegate(int x, int y);
// ...
AddDelegate del = cal.Add;
IAsyncResult ar = del.BeginInvoke(2, 5, null, null);
// 执行其他任务...
int result = del.EndInvoke(ar); // 获取返回值4. 线程同步机制
4.1 Lock 与 Monitor
- lock:最常用的语法糖,基于
Monitor.Enter/Exit实现。 - Monitor:提供
Wait、Pulse和TryEnter,可实现更复杂的同步逻辑。
4.2 Mutex 与 Semaphore
- Mutex:互斥锁,支持跨进程同步,开销比 lock 大。
- Semaphore:信号量,控制同时访问特定资源的线程数量。
5. 现代异步编程:TPL 与 async/await
现代 C# 开发建议优先使用 任务并行库 (TPL),它封装了复杂的线程管理细节,底层依然基于线程池。
5.1 Task vs Thread
- 资源消耗:
Thread创建成本高,每个线程默认占用 1MB 栈空间;Task是轻量级对象,仅在需要时由线程池驱动。 - 返回值:
Task<T>可以轻松获取异步结果,而Thread需要复杂的全局变量或闭包。 - 异常处理:
Task的异常会被捕获并封装在AggregateException中,支持更好的错误传递。
5.2 常用并行 API
Task.WhenAll(t1, t2):等待所有任务完成。Parallel.ForEach(list, item => { ... }):自动分块集合,利用多核并行处理。await Task.Delay(1000):非阻塞等待,释放当前线程回池,适合高并发 Web 环境。
5.3 线程安全集合
在多线程环境下访问集合,必须使用 System.Collections.Concurrent 命名空间下的类,避免手动写大量的 lock:
ConcurrentDictionary<K, V>:内部采用分段锁技术,性能远高于lock(dict)。ConcurrentQueue<T>:无锁队列实现,适用于生产者-消费者模式。ConcurrentBag<T>:适用于线程本地存储场景。
5.4 并行 LINQ (PLINQ)
对于大规模数据的查询和处理,可以使用 AsParallel() 将普通的 LINQ 转换为并行执行。
csharp
var results = data.AsParallel()
.Where(x => Compute(x))
.ToList();注意:并非所有查询都适合 PLINQ。如果计算任务太轻量,多线程管理的开销反而会降低性能。
5.5 并行 (Parallel) 与 异步 (Async) 的区别
- 并行 (Parallelism):同时在多个核心上执行多个任务,侧重于 计算密集型 (CPU-Bound) 任务。
- 异步 (Asynchrony):侧重于 IO 密集型 (IO-Bound) 任务。通过非阻塞方式,在等待磁盘/网络返回时释放线程去处理其他事情。
6. 实战避坑与疑难排障
6.1 UI 跨线程访问
禁止子线程直接修改 UI。
- WinForms:
control.Invoke(new Action(() => { ... })) - WPF:
Dispatcher.Invoke(() => { ... })
6.2 ASP.NET Core: "Cannot access a disposed object"
在异步任务或子线程中使用注入的 DbContext 时,极易遇到此错误。这是因为主请求线程结束后,容器自动释放了 Scoped 生命周期的数据上下文。
错误示例:
csharp
public IActionResult Test() {
Task.Run(() => _db.Users.ToList()); // 报错:_db 已释放
return View();
}正确做法: 使用 IServiceScopeFactory 手动创建生命周期作用域。
csharp
public class UserController : Controller {
private readonly IServiceScopeFactory _scopeFactory;
public UserController(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
public IActionResult Test() {
Task.Run(() => {
using (var scope = _scopeFactory.CreateScope()) {
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
var users = db.Users.ToList(); // 安全执行
}
});
return View();
}
}