1. 为什么需要多线程?
想象一下你在餐厅点餐的场景:如果只有一个服务员,他必须依次处理每位顾客的点单、上菜、结账,这时候其他顾客就只能干等着。多线程就像雇佣多个服务员,让不同任务可以并行处理——这就是多线程的核心价值。
在C#中,多线程主要解决两类问题:
- 提升响应速度:比如在GUI程序中,后台线程处理耗时计算,避免界面卡死
- 充分利用多核CPU:现代CPU都是多核心设计,单线程只能用到其中一个核心
早期我们使用System.Threading.Thread类创建线程,典型代码如下:
Thread worker = new Thread(() => { Console.WriteLine("线程ID:" + Thread.CurrentThread.ManagedThreadId); }); worker.Start();这种方式的痛点很明显:每次new Thread()都会创建一个操作系统级线程,创建和销毁成本很高。我在实际项目中就遇到过频繁创建线程导致系统资源耗尽的情况。
2. Thread的局限性
2.1 资源消耗问题
每个Thread实例都会占用约1MB的栈内存。在需要处理1000个并发请求的服务器场景中,光是线程栈就会吃掉1GB内存。更糟的是,线程切换(上下文切换)的成本高达微秒级。
2.2 管理复杂度
手动管理线程的生命周期很容易出错。比如这个典型问题:
for (int i = 0; i < 10; i++) { new Thread(() => Console.WriteLine(i)).Start(); }你以为会输出0-9?实际可能输出10个10!这是因为闭包捕获的是变量i的引用。
2.3 功能缺失
Thread原生不支持:
- 任务取消
- 返回值获取
- 异常传播
- 任务延续
这些都需要开发者手动实现,我在早期项目中就写过不少这样的样板代码。
3. Task的诞生与优势
.NET 4.0引入的Task Parallel Library (TPL)带来了全新范式。Task不是对Thread的简单封装,而是更高级的抽象——它代表一个异步操作,这个操作可能由线程池线程执行,也可能根本不用线程(比如IO完成端口)。
3.1 线程池的智慧
Task默认使用线程池,避免了频繁创建销毁线程的开销。来看个对比实验:
// Thread方式 var stopwatch = Stopwatch.StartNew(); for (int i = 0; i < 1000; i++) { new Thread(() => Thread.Sleep(10)).Start(); } Console.WriteLine($"Thread耗时:{stopwatch.ElapsedMilliseconds}ms"); // Task方式 stopwatch.Restart(); var tasks = new List<Task>(); for (int i = 0; i < 1000; i++) { tasks.Add(Task.Run(() => Thread.Sleep(10))); } Task.WaitAll(tasks.ToArray()); Console.WriteLine($"Task耗时:{stopwatch.ElapsedMilliseconds}ms");在我的i7笔记本上测试,Thread方式耗时约2000ms,而Task方式仅需150ms!
3.2 功能完备性
Task原生支持现代并发编程需要的各种特性:
返回值获取:
Task<int> task = Task.Run(() => { Thread.Sleep(1000); return 42; }); Console.WriteLine(task.Result); // 阻塞直到获取结果异常处理:
try { Task.WaitAll( Task.Run(() => { throw new Exception("Oops!"); }), Task.Run(() => { throw new Exception("Damn!"); }) ); } catch (AggregateException ex) { foreach (var e in ex.InnerExceptions) { Console.WriteLine(e.Message); } }任务取消:
var cts = new CancellationTokenSource(); Task.Run(() => { while (!cts.IsCancellationRequested) { Thread.Sleep(100); } }, cts.Token); cts.CancelAfter(1000); // 1秒后取消4. async/await革命
.NET 4.5的async/await语法让异步编程变得直观。编译器会将async方法转换为状态机,自动处理所有回调逻辑。
4.1 基本用法
async Task<int> GetPageLengthAsync(string url) { using (var client = new HttpClient()) { string content = await client.GetStringAsync(url); return content.Length; } }4.2 底层原理
编译器会将上述代码转换为类似这样的结构:
class StateMachine { int state; string url; HttpClient client; TaskCompletionSource<int> tcs; void MoveNext() { try { if (state == 0) { client = new HttpClient(); var task = client.GetStringAsync(url); task.ContinueWith(_ => MoveNext()); state = 1; return; } string content = task.Result; tcs.SetResult(content.Length); } catch (Exception ex) { tcs.SetException(ex); } } }4.3 常见误区
- async void:只用于事件处理,无法捕获异常
- 死锁风险:在UI线程调用.Result会导致死锁
// 错误示例 var result = GetPageLengthAsync(url).Result; // 正确做法 var result = await GetPageLengthAsync(url);5. 实战选择指南
5.1 什么时候用Thread?
- 需要精细控制线程优先级(如实时系统)
- 需要长时间运行的后台线程
- 需要单线程单元(STA)的COM交互
5.2 什么时候用Task?
- 大多数I/O密集型操作(文件、网络)
- CPU密集型并行计算(配合Parallel类)
- 需要任务组合、取消等高级功能时
5.3 性能对比
在我的压力测试中(10000次并发):
| 指标 | Thread方式 | Task方式 |
|---|---|---|
| 内存占用(MB) | 1024 | 32 |
| 创建时间(ms) | 2100 | 120 |
| 上下文切换(次) | 9800 | 150 |
6. 高级技巧
6.1 任务组合
// 并行执行多个任务 var tasks = new List<Task<string>>(); for (int i = 0; i < 10; i++) { tasks.Add(FetchDataAsync($"api/data/{i}")); } // 等待任意一个完成 var firstResult = await Task.WhenAny(tasks); // 等待全部完成 var allResults = await Task.WhenAll(tasks);6.2 超时处理
var downloadTask = DownloadFileAsync(url); var timeoutTask = Task.Delay(5000); var completedTask = await Task.WhenAny(downloadTask, timeoutTask); if (completedTask == timeoutTask) { throw new TimeoutException(); }6.3 限制并发度
var semaphore = new SemaphoreSlim(5); // 最大5个并发 var tasks = urls.Select(async url => { await semaphore.WaitAsync(); try { return await DownloadAsync(url); } finally { semaphore.Release(); } }); await Task.WhenAll(tasks);7. 常见坑与解决方案
坑1:闭包陷阱
for (int i = 0; i < 5; i++) { Task.Run(() => Console.WriteLine(i)); // 可能输出5个5 } // 正确做法 for (int i = 0; i < 5; i++) { int temp = i; Task.Run(() => Console.WriteLine(temp)); }坑2:异步构造方法C#不允许async构造方法,替代方案:
class MyClass { private MyClass() {} public static async Task<MyClass> CreateAsync() { var instance = new MyClass(); await instance.InitAsync(); return instance; } }坑3:忘记ConfigureAwait在库代码中应该使用:
await SomeTask().ConfigureAwait(false);避免不必要的上下文切换。
从Thread到Task的演进,反映了C#语言设计者对开发者体验的不懈追求。在实际项目中,我逐渐将旧代码中的Thread替换为Task,不仅代码量减少了40%,性能还提升了3-5倍。特别是在最近的一个高并发服务改造中,Task+async/await的组合让我们用20台服务器扛住了原本需要100台服务器的流量。