1. 介绍
如果把进程想象成一家超市,线程(thread)就像不同收银台的收银员,他们同时为顾客服务。所有收银员都在同一个店里,但每个人并行完成自己的工作,这样效率更高。进程内部的线程允许同时执行多项任务,共享资源并在一个应用内协调工作。最主要的优点是:如果 CPU 支持并行,多线程能真正并行运行。
为什么需要线程(实战场景)
为啥我们要启动多个线程?举几个现实中的例子:
- 你在写带 GUI 的应用,不想在长时间操作时程序“卡死”。
- 你需要同时下载多个文件。
- 在游戏里,敌人需要独立思考,不受其他逻辑阻塞。
令人惊讶的是,很多程序仍然因为不熟练用线程而出现“卡顿”。今天我们学会如何避免这些坑。
Thread 类:手工多线程的基础
Thread 类是 .NET 多线程编程里的老古董。尽管后来出现了更现代的工具(Task、async/await),但学会用 Thread 仍然有价值,尤其想从零开始理解线程内情的时候。
创建线程的步骤
- 创建一个 Thread 对象,传入将在线程中执行的方法。
- 用 Start() 启动线程。
- (可选)观察运行情况——毕竟一切可能是并行的!
2. 用 Thread 启动线程
来实操感受并行的魔力。我们给条件应用加个小类,让它数到某个数并输出进度,学习如何把这项工作放到单独线程里运行。
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("主线程启动啦!");
// 创建一个 Thread 对象,传入要在线程中执行的方法
Thread workerThread = new Thread(CountToTen);
// 启动新线程
workerThread.Start();
// 主线程也在干点事:输出点点...
for (int i = 0; i < 5; i++)
{
Console.Write(".");
Thread.Sleep(500); // 为了看得清楚延迟一下
}
Console.WriteLine("\n主线程结束啦!");
}
static void CountToTen()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"[线程] 计数:{i}");
Thread.Sleep(400);
}
Console.WriteLine("[线程] 完成!");
}
}
发生了什么?
你会在控制台看到点点和 "计数:X" 交错出现。这是多线程的第一个迹象!主线程打印它的点点,而新线程数到 10。它们互不干扰,就像乐队里两个乐手:一个打鼓,一个弹琴,各自发声,合起来就是一首歌。
3. 怎么给线程传数据?
有时候线程不仅要知道做什么,还要知道操作哪个数据。如果线程的方法带参数,怎么把参数传进去?
方法一:用 lambda(匿名方法)
int bounds = 7;
Thread t = new Thread(() => CountToNumber(bounds));
t.Start();
static void CountToNumber(int n)
{
for (int i = 1; i <= n; i++)
{
Console.WriteLine($"[线程] {i} / {n}");
Thread.Sleep(300);
}
}
这里我们把想要调用的方法包在 lambda 里,从而传入参数。这很常见,因为 Thread 要的是无参方法(ThreadStart)。
方法二:用 ParameterizedThreadStart
也可以用专门的委托 ParameterizedThreadStart,它接受一个 object 类型的参数。
Thread t = new Thread(CountToNumberObject);
t.Start(12);
static void CountToNumberObject(object? n)
{
int max = (int)n!;
for (int i = 1; i <= max; i++)
{
Console.WriteLine($"[线程] {i} / {max}");
Thread.Sleep(200);
}
}
参数类型是 object,所以需要做类型转换。不是特别优雅,但能用。现代 C# 更推荐用 lambda 的方式。
4. 线程生命周期的管理
来看看 Thread 提供了哪些实用东西。
| 属性 / 方法 | 用途 |
|---|---|
|
启动线程(执行创建时指定的方法) |
|
等待线程结束(阻塞调用线程直到另一个线程完成) |
|
指示线程当前是否在运行(true/false) |
|
给线程命名(对调试很有帮助) |
|
获取表示当前线程的对象 |
|
让当前线程休眠 ms 毫秒 |
示例:等待线程结束
有时候希望主线程等辅助线程干完再继续。
Thread t = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"[第二线程] {i}");
Thread.Sleep(300);
}
});
t.Start();
Console.WriteLine("[主线程] 等待第二个线程结束...");
t.Join(); // 主线程在这里等
Console.WriteLine("[主线程] 第二个线程已结束!");
没有 Join(),程序可能在线程还没结束时就退出了。有了 Join(),主线程会耐心等所有任务完成。
5. 有用的小细节
给线程命名:别弄混了
调试时给线程起个名字会很方便:
Thread t = new Thread(() =>
{
Console.WriteLine($"这在线程里执行:{Thread.CurrentThread.Name}");
});
t.Name = "计数线程";
t.Start();
当线程变多,每个线程做不同事情时,这会很有帮助。
限制和现实中的趋势
直接用 Thread 手动管理线程在现代应用里越来越少见。实际项目通常用更强大的工具(Task、async/await),我们之后会详细讲。不过理解线程基础很重要,适用于:
- 理解 C# 和 .NET 的“幕后”运作。
- 面试时(有时候会被问到 Thread 和 Task 的区别)。
- 排查和修复大型“遗留”应用里的复杂问题。
总结图:线程的生命周期
stateDiagram-v2
[*] --> New: Thread 已创建
New --> Running: Start()
Running --> Stopped: 方法结束
Stopped --> [*]
现在你已经会自己创建并启动 C# 线程了。你不再只是火车上的乘客,而是能同时操控好几节车厢的司机!接下来会讲线程生命周期、管理、同步以及并行的新天地。
6. 使用 Thread 时的常见错误和小技巧
错误 №1:忘记启动线程。
常见情况是创建了 Thread 对象,却忘了调用 Start(),结果线程根本没跑,排查起来挺迷惑的。
错误 №2:在没有同步的情况下修改共享数据。
如果多个线程操作同一个变量但没加保护,问题肯定会出现!就像两个收银员从同一个钱箱找零——很快会乱套。
错误 №3:使用过时且不安全的方法。
别用 Thread.Suspend()、Thread.Resume() 这类方法——它们被认为危险且过时。用其他方式管理线程生命周期。
错误 №4:线程内未处理的异常。
如果线程里抛了异常但没捕获,线程会终止,而主线程可能完全不知道这事!把线程代码包到 try-catch 里,记录错误。
Thread t = new Thread(() =>
{
try
{
// ... 你的代码
}
catch (Exception ex)
{
// 记录错误
Console.WriteLine($"[线程] 错误:{ex.Message}");
}
});
t.Start();
GO TO FULL VERSION