新增进阶知识点汇总文档
在之前C#高级知识点基础上,新增:Task与Thread、CLR‑IL2CPP、闭包深度原理三大部分
一、Task 和 Thread
1.核心定义
- Thread(线程):操作系统原生线程,一条独立的执行流,由OS直接调度。新建线程属于重量级操作,开销高。
- Task:.NET托管层高层任务抽象(工作单元),并不一定对应一条操作系统线程;默认复用线程池
ThreadPool里面已经建好的线程。
2.关键区别对照表
| 对比项 | Thread | Task |
|---|---|---|
| 层级 | 操作系统原生线程 | 托管层抽象任务 |
| 资源来源 | 新建一条全新内核线程 | 优先复用线程池线程 |
| 启动开销 | 高 | 低(复用线程) |
| 返回结果 | 无法便捷获取返回值 | Task<T>支持返回结果 |
| 等待方式 | Thread.Join() 阻塞当前线程 |
await 非阻塞等待,不会卡住线程 |
| 异常处理 | 异常直接崩溃进程,捕获困难 | 内置异常状态,可try‑catch捕获 |
| 取消 | 无原生取消接口,手动标记 | 自带标准 CancellationToken |
3.重大误区
async‑await 不会自动新开子线程;只有 Task.Run() 才会把任务调度到线程池子线程。
IO等待任务(文件、网络)等待期间不占用任何线程,线程归还线程池。
4.适用场景
- Thread:长期常驻后台循环任务(心跳服务)
- Task:短任务、IO异步、并发任务;优先搭配
async/await使用
5.线程池 ThreadPool
CLR提前创建一批常驻线程;Task用完线程立刻归还,避免频繁创建销毁原生线程带来的性能损耗。
二、CLR 与 IL2CPP
1、标准 .NET(Mono‑CLR) 运行流程
C#源码 →编译器→ IL中间语言 → CLR虚拟机(JIT即时编译) → 机器码
- IL:跨CPU架构的中间代码
- CLR:.NET虚拟机,职责包含:JIT即时编译、GC垃圾回收、类型安全校验、线程管理、异常处理
- JIT:运行的时候才把IL翻译成CPU机器码
2、IL2CPP(Unity)
全称:IL‑to‑C++,Unity的AOT编译后端,用来替代CLR虚拟机 编译流程:
C#源码 → IL中间代码 → IL2CPP转换器 → C++源代码 → C++编译器 → 原生机器码
打包阶段提前全部编译完成,运行时不再需要虚拟机、不再需要JIT。
3、二者关系(互斥二选一)
- Mono后端(编辑器、Windows):运行在Mono‑CLR虚拟机 + JIT
- IL2CPP后端(安卓、iOS):没有CLR虚拟机,纯原生代码执行
4、IL2CPP保留/丢失的能力
✅保留:C#语法、async/await、委托、泛型、GC(IL2CPP自带独立GC,不是CLR‑GC) ❌丢失:JIT动态代码生成;Emit动态发射代码功能不可用;反射能力受限。
5.Mono‑CLR vs IL2CPP对比
| 项目 | Mono‑CLR | IL2CPP |
|---|---|---|
| 编译模式 | JIT运行时编译 | AOT提前编译 IL→C++ |
| 虚拟机 | 存在Mono‑CLR虚拟机 | 无虚拟机 |
| 动态代码生成 | 支持 | 禁止 |
| GC | Mono GC | IL2CPP自有GC |
三、闭包 深度原理(重点‑内存机制)
1.闭包本质
Lambda/匿名函数捕获外部函数局部变量,匿名函数 + 被捕获变量,整体构成闭包。
核心底层行为:变量提升 ❌不是:栈上int拷贝一份副本放到堆 ✅是:编译器重写代码,直接把被捕获的局部变量从栈上搬家到托管堆的隐藏闭包类里面;原栈上变量彻底消失。全程只有一份内存。
2.编译器转换伪代码示例
源代码
Action Create()
{
int num = 10;
return ()=> Console.WriteLine(num);
}
编译器生成隐藏闭包类:
private sealed class Closure
{
public int num; //原来的局部变量升级为堆对象字段
public void Run(){ Console.WriteLine(num); }
}
从此以后代码中所有num,全部访问堆里面同一个字段。
3.两大核心问题解答
- 为什么修改原变量,Lambda读取的值也跟着变? 所谓“原变量”和Lambda捕获到的变量,指向堆上同一块内存;不存在栈副本,修改一处全部生效。
- 函数结束栈帧销毁,为什么捕获的变量仍然存活? 栈帧只清理栈内存;闭包对象存放在托管堆;只要委托还持有闭包对象的引用,GC就不会回收,生命周期不受函数栈影响。
4.经典坑点:for循环捕获变量
List<Action> list = new List<Action>();
for (int i = 0; i < 3; i++)
{
list.Add(() => Console.WriteLine(i));
}
输出结果:3, 3, 3
- 原理:
i定义在for循环外部,仅生成唯一一个变量;被提升到堆上,所有Lambda共享同一个i。循环结束i=3。 - 修复方案:循环体内新建临时副本变量
int temp = i; //每一轮循环生成全新独立变量
list.Add(()=>Console.WriteLine(temp));
5.闭包‑异步延迟捕获陷阱
异步任务延迟执行,循环早已跑完,捕获到循环变量为最终值;同样使用临时副本解决。
6.闭包引发GC内存泄漏风险
事件回调、长期存活的委托,会一直持有闭包对象;被闭包捕获的引用类型对象,即使物体Destroy()也无法被GC回收。
解决方案:事件用完手动
‑=解绑委托。
7.什么时候不会生成闭包?
Lambda只访问类的成员字段、静态变量,没有捕获函数内局部变量,不会触发变量提升,不会生成闭包类。
8.关键误区区分:闭包提升 ≠ 装箱
装箱:栈上的值拷贝一份放到object堆实例;
闭包提升:编译器重写代码,变量直接一开始就放在堆上的闭包类,没有拷贝过程。


0 条回复
还没有留言,来说点什么吧。