新增进阶知识点汇总文档


在之前C#高级知识点基础上,新增:Task与Thread、CLR‑IL2CPP、闭包深度原理三大部分

一、Task 和 Thread

1.核心定义

  1. Thread(线程):操作系统原生线程,一条独立的执行流,由OS直接调度。新建线程属于重量级操作,开销高。
  2. 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、二者关系(互斥二选一)

  1. Mono后端(编辑器、Windows):运行在Mono‑CLR虚拟机 + JIT
  2. 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.两大核心问题解答

  1. 为什么修改原变量,Lambda读取的值也跟着变? 所谓“原变量”和Lambda捕获到的变量,指向堆上同一块内存;不存在栈副本,修改一处全部生效。
  2. 函数结束栈帧销毁,为什么捕获的变量仍然存活? 栈帧只清理栈内存;闭包对象存放在托管堆;只要委托还持有闭包对象的引用,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堆实例; 闭包提升:编译器重写代码,变量直接一开始就放在堆上的闭包类,没有拷贝过程。


25 8 月, 2026Garfield God学习笔记阅读 1 次

0 条回复

还没有留言,来说点什么吧。

留言