08-C#.Net-Thread-学习笔记
一、多线程基础概念
多线程的三大特点:
- 异步执行:不阻塞主线程,多件事可以同时进行
- 效率高:充分利用 CPU 等计算机资源
- 无序性:多个线程的执行顺序不可预测,无法控制
⚠️ 正因为无序性,多线程调试困难,通常只能通过写日志、输出结果、结合线程 ID 来分析问题。
// 获取当前线程 ID,用于区分不同线程
int id = Thread.CurrentThread.ManagedThreadId;
二、Thread 类
基本概念
- 来源:
System.Threading命名空间 - 出现时间:.NET Framework 1.0
- 类型:密封类(
sealed class),不可被继承 - 作用:操作计算机线程资源的帮助类库
开启线程
方式一:ThreadStart(无参数)
ThreadStart threadStart = new ThreadStart(() =>
{
DoSomething("Richard");
});
Thread thread = new Thread(threadStart);
thread.Start();
方式二:ParameterizedThreadStart(带参数)
ParameterizedThreadStart pts = new ParameterizedThreadStart(obj =>
{
Console.WriteLine($"参数: {obj}");
});
Thread thread = new Thread(pts);
thread.Start("传递的参数");
常用 API
线程控制
thread.Suspend(); // 暂停线程(不会立即停止)
thread.Resume(); // 恢复执行
thread.Abort(); // 停止线程(向线程抛出异常)
Thread.ResetAbort(); // 取消停止,让线程继续执行
❌ Suspend、Resume、Abort 均已标记为过时(Obsolete),实际开发中不应使用。线程无法真正从外部被强制终止,这些方法只是通过异常机制间接干预,存在安全隐患。
线程等待
方式一:观望式轮询(不推荐)
while (thread.ThreadState != ThreadState.Stopped)
{
Thread.Sleep(500); // 每 500ms 检查一次
}
❌ 持续轮询会浪费 CPU 时间片,不推荐。
方式二:Join 等待(推荐)
thread.Join(); // 无限等待,直到线程结束
thread.Join(500); // 最多等待 500ms,过时不候
thread.Join(TimeSpan.FromMilliseconds(500)); // 同上,TimeSpan 写法
✅ Join() 是最简洁直接的线程等待方式。
前台线程 vs 后台线程
thread.IsBackground = true; // 后台线程:程序关闭时,线程立即终止
thread.IsBackground = false; // 前台线程:程序关闭时,等待线程执行完毕再退出
- 默认:通过
new Thread()创建的线程是前台线程 - ThreadPool 中的线程全部是后台线程
- 主线程(Main 方法所在线程)是前台线程
⚠️ 如果忘记将长时间运行的线程设为后台线程,可能导致程序关闭后进程仍然存活。
线程优先级
thread.Priority = ThreadPriority.Highest; // 最高优先级
thread.Priority = ThreadPriority.Lowest; // 最低优先级
⚠️ 设置优先级只是提高被优先调度的概率,操作系统不保证严格按优先级执行,不能依赖它来控制执行顺序。
三、Thread 扩展封装
问题背景
多线程天然无序,但有时需要:
- 两个操作都在新线程中执行
- 同时保证它们的执行顺序
方案一:顺序执行两个委托
把两个委托放进同一个线程,天然保证顺序:
private void CallBackThread(ThreadStart threadStart, Action action)
{
Thread thread = new Thread(() =>
{
threadStart.Invoke(); // 先执行
action.Invoke(); // 后执行
});
thread.Start();
}
方案二:带返回值的异步执行
核心思路:立即开启新线程执行,返回一个"取结果的委托",调用方在需要结果时才触发等待。
private Func<T> CallBackFunc<T>(Func<T> func)
{
T result = default;
Thread thread = new Thread(() =>
{
result = func.Invoke(); // 新线程中执行
});
thread.Start(); // 立即开启,不阻塞
// 返回一个延迟取值的委托
return new Func<T>(() =>
{
thread.Join(); // 调用时才等待线程完成
return result;
});
}
使用示例:
Func<int> asyncFunc = CallBackFunc(() =>
{
Thread.Sleep(5000);
return DateTime.Now.Year;
});
// 这里不阻塞,可以继续做其他事
Console.WriteLine("继续执行其他任务...");
// 需要结果时才等待
int result = asyncFunc.Invoke();
⚠️ .NET Core 中 Delegate.BeginInvoke / EndInvoke 已不再支持,不能用于异步执行委托,需要用上述方式或 Task 替代。
四、ThreadPool 线程池
为什么需要线程池
Thread 的问题:
- API 繁多,控制复杂
- 线程数量需要程序员手动管理,容易滥用
- 频繁创建/销毁线程,系统开销大
ThreadPool 的优势:
- .NET Framework 2.0 引入,采用池化思想
- 预先创建线程放入池中,需要时直接取用,用完自动归还
- 自动管理线程数量,防止资源耗尽
- API 更简洁,去掉了 Thread 中不必要的方法
申请线程执行任务
ThreadPool.QueueUserWorkItem(obj =>
{
DoSomething(obj.ToString());
}, "参数");
线程等待:ManualResetEvent
ThreadPool 没有 Join(),需要用 ManualResetEvent 实现等待。
ManualResetEvent resetEvent = new ManualResetEvent(false); // 初始为未触发
ThreadPool.QueueUserWorkItem(obj =>
{
DoSomething(obj.ToString());
resetEvent.Set(); // 任务完成,发出信号
}, "Richard");
// 主线程可以继续做其他事
Console.WriteLine("主线程继续执行...");
resetEvent.WaitOne(); // 阻塞,直到 Set() 被调用
Console.WriteLine("子线程已完成");
工作原理:
new ManualResetEvent(false)— 初始状态为"未触发"- 子线程任务完成后调用
Set()— 状态变为"已触发" - 主线程调用
WaitOne()— 阻塞等待,直到状态变为"已触发"才继续
控制线程数量
ThreadPool.SetMinThreads(4, 4); // 设置最小工作线程数和 IO 线程数
ThreadPool.SetMaxThreads(8, 8); // 设置最大工作线程数和 IO 线程数
ThreadPool.GetMinThreads(out int minWorker, out int minIO);
ThreadPool.GetMaxThreads(out int maxWorker, out int maxIO);
- 第一个参数:工作线程数(Worker Threads),用于 CPU 密集型任务
- 第二个参数:IO 线程数(Completion Port Threads),用于 IO 操作
❌ 强烈不建议手动设置线程池大小。原因:
- 这是进程级别的全局设置
Task、Parallel、async/await底层都使用线程池,修改后会影响所有这些功能- 设置不当容易引发性能问题甚至死锁
五、Thread vs ThreadPool 对比
| 特性 | Thread | ThreadPool |
|---|---|---|
| 出现时间 | .NET 1.0 | .NET 2.0 |
| 线程管理 | 手动创建和销毁 | 自动管理,复用线程 |
| API 复杂度 | 复杂,功能多 | 简单,易用 |
| 性能 | 频繁创建销毁,开销大 | 复用线程,性能好 |
| 线程等待 | Join() |
ManualResetEvent |
| 默认线程类型 | 前台线程 | 后台线程 |
| 适用场景 | 需要精细控制的长时间任务 | 大量短时间并发任务 |
六、注意事项与最佳实践
✅ 短时间任务优先使用 ThreadPool
✅ 使用 Join() 进行 Thread 的线程等待
✅ 使用 ManualResetEvent 进行 ThreadPool 的线程同步
✅ 长时间运行的线程记得设置 IsBackground = true
❌ 避免使用 Abort()、Suspend()、Resume()
❌ 不要手动设置线程池大小
❌ 不要在生产环境依赖线程优先级来控制执行顺序
⚠️ 多个线程访问共享资源时需要加锁(lock、Monitor 等),否则会出现数据竞争问题。
⚠️ 线程内部的异常不会自动传播到主线程,必须在线程内部自行处理。
⚠️ 在实际项目中,更推荐使用 Task 和 async/await,它们是对线程池的更高层封装,代码更简洁,异常处理和取消机制也更完善。
本文来自博客园,作者:龙猫•ᴥ•,转载请注明原文链接:https://www.cnblogs.com/nullcodeworld/p/19740009

浙公网安备 33010602011771号