知识点补充:
- 进程 :是操作系统进行资源分配和隔离的基本单位。每个进程拥有独立的虚拟地址空间、数据段、代码段以及系统资源(如打开的文件)。进程间通信(IPC)成本高。
- 线程 :是操作系统进行 CPU 调度和执行 的基本单位,是进程内的一个子任务。同一进程的多个线程共享进程的内存和资源,因此线程间通信更高效,但同步不当易导致数据错乱。线程的创建、切换由操作系统内核管理。
- 协程 :是一种用户态 的轻量级线程,其调度完全由用户程序控制 (协作式调度),而非操作系统内核。协程在单线程内实现多任务调度,切换代价极低 ,且能避免无意义的线程阻塞,极大提升并发吞吐量,尤其适合 I/O 密集型场景。在 Java 中,Project Loom 引入的虚拟线程即是协程理念的官方实现。
协程接口:

迭代器接口:

伪代码:
可以看到迭代器和协程的接口实现的方法,但方法的内容执行了哪些操作了,这里直接继承重写一个伪代码来梳理具体的操作逻辑。
cs
public class car
{
public string Brand { get; }
public int Year { get; }
public car(string brand, int year)
{
Brand = brand;
Year = year;
}
// 打印
public override string ToString() => $"{Brand} ({Year})";
}
public class cars : IEnumerable
{
// 存储汽车列表的内部数组
private car[] carlist;
// 构造函数:初始化汽车数组
public cars()
{
carlist = new car[6]
{
new car("Ford",1992),
new car("Fiat",1988),
new car("Buick",1932),
new car("Ford",1932),
new car("Dodge",1999),
new car("Honda",1977)
};
}
// 实现IEnumerable的核心方法:返回迭代器实例
public IEnumerator GetEnumerator()
{
// 把内部的car数组传给自定义迭代器,返回迭代器对象
return new MyEnumerator(carlist);
}
}
public class MyEnumerator : IEnumerator
{
// 持有要遍历的汽车数组(由外部cars类传入)
public car[] carlist;
// 核心:状态标记!记录当前遍历到的位置
int position = -1;
// 构造函数:接收要遍历的数组
public MyEnumerator(car[] list)
{
carlist = list;
}
// IEnumerator核心方法1:推进迭代器状态(协程MoveNext()的原型)
public bool MoveNext()
{
// 状态+1(比如从-1→0,对应第一个元素;0→1对应第二个,以此类推)
position++;
// 返回是否还有下一个元素:有则true(继续遍历),无则false(遍历结束)
return (position < carlist.Length);
}
// IEnumerator核心方法2:重置迭代器状态
public void Reset()
{
position = -1; // 回到初始状态
}
// IEnumerator核心属性:获取当前状态对应的元素(协程的Current属性原型)
public object Current
{
get
{
try
{
// 返回当前position对应的汽车对象
return carlist[position];
}
catch (IndexOutOfRangeException)
{
// 越界时抛标准异常
throw new InvalidOperationException();
}
}
}
}
// 使用
var cars = new cars();
// 打印
foreach (var number in cars)
{
Debug.Log($"Main: {number}");
}
可以看到foreach迭代器,每次迭代器都会调用 IEnumerator GetEnumerator(); 方法,获取协程。协程是会记录更新每次的数组状态,执行返回对应的数组状态的值,这个模式就是状态机的原理。
原理:
unity的协程就是当你在IEnumerator 的函数里写yield时,C# 编译器会自动生成一个状态机类。
yield的实现相对于是状态机的添加不同行为节点的方法。
MoveNext会按照你的添加顺序依次执行节点
MoveNext():控制协程 "走下一步",返回true表示还有后续步骤,false表示协程结束;Current:返回当前要等待的对象(比如WaitForSeconds)- Reset :重置状态。
上面我们是我们实现的伪代码是直接返回值,
cs
public IEnumerable<int> CountUp()
{
for (int i = 1; i <= 5; i++)
{
yield return i; // 暂停并返回当前值
}
}
但实际上不是直接return返回值,而是暂停当前返回值的操作,需要等到下一次**MoveNext** 更新Current需要暂停的对象,旧的Current的对象就恢复返回。
那么核心问题就是什么时候调用**MoveNext, ** 在上面伪代码中直接foreach来调用的movenext,那么问题就是没有等待。
unity的原生协程之所以是运行在主线程并且需要依靠MonoBehaviour的生命周期,是因为untiy的协程管理会在每帧的更新都去查询(引擎每帧轮询 )当前的启动没有完成的协程当前的等待对象是否完成,完成就调用协程的MoveNext方法,没有就不执行操作。
原生协程依靠主线程执行,但是不会阻塞,
举个例子:邮递员需要人在家的时候才能把信交付出去,并且拿到新的信,没有新的信就结束,邮递员会每次查看需要送的信如果人不在家,邮递员不会等,回去下一个地址查看,没有送出的信会第二天继续流程。不会因为等待某人,耽误行程导致不能按时下班或者第二天上班迟到。依然会按时上下班。
简单说:原生协程就是 **"状态机 + 引擎每帧轮询"** 的组合,伪异步执行代码。
问题:
1.因为原生协程是依靠untiy的MonoBehaviour的生命周期实现的轮询,所有纯C#的情况,是无法使用协程的,因为纯C#的代码是上面生命周期来轮询执行协程的。
2.GC 严重:每次创建WaitForSeconds/WaitUntil都会 new 对象,频繁触发垃圾回收;
3.拓展性差:自定义等待条件需要写额外逻辑,无法统一管理。
自定义:
原生协程无法在纯 C# 类中使用,核心原因是调度权完全绑定 Unity 引擎和 MonoBehaviour:
- 启动层面:
StartCoroutine()是MonoBehaviour的成员方法,纯 C# 类没有这个方法,无法启动协程; - 驱动层面:Unity 引擎只对挂载了
MonoBehaviour的对象管理协程调度,纯 C# 类没有 "每帧执行" 的入口(Update/FixedUpdate),协程的MoveNext()无法被驱动; - 生命周期层面:原生协程的生命周期和所属 MonoBehaviour 绑定,纯 C# 类无生命周期,引擎无法感知其存在。
知道了原生协程的问题,就可以自定义进行解决:
自定义需要实现的目标:
1.适配纯C#的情况,
2.优化GC,可以使用对象池
实现步骤:
1.对象池
为伪代码使用,就不复杂化了。就简单的实现创建和归还功能即可。
cs
/// <summary>
/// 简化对象池
/// </summary>
/// <typeparam name="T"></typeparam>
public class ObjectPool<T> where T : new()
{
private readonly Stack<T> _pool = new Stack<T>();
private readonly int _maxSize;
public ObjectPool(int maxSize = 1024)
{
_maxSize = maxSize;
}
// 获取对象
public T Get()
{
return _pool.Count > 0 ? _pool.Pop() : new T();
}
// 释放对象
public bool Release(T obj)
{
if (_pool.Count < _maxSize)
{
_pool.Push(obj);
return true;
}
return false;
}
}
2.协程的等待器
实现两个必须的基本功能
(1)获取是否等待完成的方法。
作用:每次轮询都会触发, 及每次轮询都会执行该方法,更新执行进度。
(2)等待结束的资源释放方法。
作用:就是持有的内容资源,不使用就释放掉。
并且实现一个简单的定时等待的方法。
提供一个暂停返回值直到指定等待时间的方法。
cs
/// <summary>
/// 抽象的等待器对象
/// </summary>
public abstract class Waiter
{
protected internal abstract bool KeepWaiting(); // 是否继续等待
protected internal abstract void Dispose(); // 释放资源
}
/// <summary>
/// 实现一个时间等待器对象
/// </summary>
public class TimeWaiter : Waiter
{
private static readonly ObjectPool<TimeWaiter> _pool = new ObjectPool<TimeWaiter>(); // 对象池
public float Seconds; // 等待时间
public float Timer; // 计时器
public Func<float> GetDeltaTime; // 获取时间增量的函数,需要有外部赋值返回,使用func委托
protected internal override bool KeepWaiting()
{
Timer += GetDeltaTime();
return Timer >= Seconds;
}
protected internal override void Dispose()
{
// 重置状态
Seconds = 0;
Timer = 0;
GetDeltaTime = null;
// 归还对象池
_pool.Release(this);
}
// 静态工厂方法获取TimeWaiter实例
public static TimeWaiter WaitTime(float seconds, Func<float> getDeltaTime)
{
// 数据校验
if (seconds < 0) throw new ArgumentOutOfRangeException(nameof(seconds), "等待时间不能为负数");
var waiter = _pool.Get();
waiter.Seconds = seconds;
waiter.Timer = 0;
waiter.GetDeltaTime = getDeltaTime;
return waiter;
}
}
3.协程的句柄
协程句柄的核心意义可总结为 3 句话:
- 简化控制:外部无需关心协程的底层调度器、迭代器,只用句柄就能停止 / 释放协程;
- 精准安全:每个句柄对应一个协程,精准控制单个协程,且支持资源清理,避免内存泄漏;
- 解耦扩展:统一控制接口,适配不同调度器实现,不随底层逻辑变化而修改外部代码。
cs
// 协程的句柄
public struct CoroutineHandle : IDisposable, IEquatable<CoroutineHandle>
{
private ICoroutineScheduler _scheduler; // 调度器
private IEnumerator<Waiter> _coroutine; // 协程枚举器
public CoroutineHandle(ICoroutineScheduler scheduler, IEnumerator<Waiter> coroutine)
{
_scheduler = scheduler;
_coroutine = coroutine;
}
public void Stop()
{
if ( _coroutine != null )
{
_scheduler.StopCoroutine(in _coroutine);
_coroutine = null;
}
}
public void Dispose()
{
_coroutine = null;
_scheduler = null;
}
public bool Equals(CoroutineHandle other)
{
return _scheduler == other._scheduler && Equals(_coroutine, other._coroutine);
}
}
4.协程的调度器
实现必要的协程方法
Run:轮询的方法实现。
StopCoroutine:暂停协程的方法实现。
RunCoroutine:启动协程的方法实现。
cs
// 调度器接口
public interface ICoroutineScheduler
{
void Run();
void StopCoroutine(in IEnumerator<Waiter> coroutine);
CoroutineHandle RunCoroutine(in IEnumerator<Waiter> coroutine);
}
// 协程的调度器
public class CoroutineScheduler : ICoroutineScheduler
{
// 记录所有运行中的协程-> 使用链表方便增删操作
private readonly LinkedList<IEnumerator<Waiter>> _runningCoroutines = new LinkedList<IEnumerator<Waiter>>();
// 对应原生协程中的Update方法 -> 每帧调用一次/定时调用一次
public void Run()
{
var node = _runningCoroutines.First; // 获取第一个节点
while (node != null)
{
var coroutine = node.Value; // 获取协程枚举器
var nextNode = node.Next; // 先缓存下一个节点,避免移除当前节点后遍历中断
// 初始状态:Current为null,先调用MoveNext()启动协程
if (coroutine.Current == null && !coroutine.MoveNext())
{// 协程为空,并且无法移动到下个状态节点,说明协程结束
_runningCoroutines.Remove(node);
// 继续下一个节点
node = nextNode;
continue;
}
// 协程没有结束 检查当前等待器是否完成等待
var currentWaiter = coroutine.Current as Waiter; // 获取当前等待器
if (currentWaiter != null && currentWaiter.KeepWaiting())
{
// 结束等待释放资源再移除,跳过到下一个协程
currentWaiter.Dispose();
if (!coroutine.MoveNext())
{
// 协程结束,移除
_runningCoroutines.Remove(node);
}
}
node = nextNode;
}
}
public CoroutineHandle RunCoroutine(in IEnumerator<Waiter> coroutine)
{
// 生成协程句柄
var handle = new CoroutineHandle(this, coroutine);
_runningCoroutines.AddLast(coroutine);
return handle;
}
public void StopCoroutine(in IEnumerator<Waiter> coroutine)
{
_runningCoroutines.Remove(coroutine);
}
}
5.使用测试
cs
public class PureCSharpClass
{
private readonly CoroutineScheduler _scheduler = new CoroutineScheduler();
private float _currentTime;
// 封装:启动协程的公共方法
public CoroutineHandle StartCustomCoroutine(IEnumerator<Waiter> coroutine)
{
return _scheduler.RunCoroutine(coroutine);
}
// 封装:停止协程的公共方法
public void StopCustomCoroutine(IEnumerator<Waiter> coroutine)
{
_scheduler.StopCoroutine(coroutine);
}
// 模拟Update
public void Update(float deltaTime)
{
_currentTime += deltaTime;
_scheduler.Run();
Debug.Log($"当前时间:{_currentTime:F1}s");
}
// 协程逻辑
public IEnumerator<Waiter> TestCoroutine()
{
Debug.Log("纯C#协程启动");
yield return TimeWaiter.WaitTime(4f, () => 0.01f);
Debug.Log("纯C#协程等待4秒后执行");
yield return TimeWaiter.WaitTime(1f, () => 0.01f);
Debug.Log("纯C#协程结束");
}
}
// 测试方法
public void Test()
{
// 自定义协程测试
var pureClass = new PureCSharpClass();
// 启动协程
pureClass.StartCustomCoroutine(pureClass.TestCoroutine());
// 模拟10帧执行(每帧0.1秒)
for (int i = 0; i < 1000; i++)
{
pureClass.Update(0.01f);
System.Threading.Thread.Sleep(10); // 模拟帧间隔
}
}
补充知识点:
1.对struct、abstract class、interface的使用
| 特性 | struct(结构体) | abstract class(抽象类) | interface(接口) |
|---|---|---|---|
| 类型 | 值类型 | 引用类型 | 引用类型(约定) |
| 继承 | 不支持继承(隐式继承ValueType) | 支持单继承 | 支持多实现 |
| 实例化 | 可实例化 | 不可实例化 | 不可实例化 |
| 默认实现 | 可以有 | 可以有具体和抽象方法 | C#8.0+可以有默认实现 |
| 构造函数 | 可有(必须带参数) | 可有 | 无构造函数 |
| 字段 | 可以有 | 可以有 | C#8.0+可以有静态字段 |
| 访问修饰符 | 成员默认private | 默认继承规则 | 默认public |
选择指南
何时使用struct?
-
数据大小小于16字节
-
需要值语义(拷贝时完全复制)
-
不需要继承和多态
-
性能敏感,避免堆分配
-
数据不可变时更安全
特点:
-
值类型,栈上分配(通常)
-
适合小型数据结构
-
复制时是值拷贝
何时使用abstract class?
-
多个相关类共享代码
-
需要提供基础实现
-
使用模板方法模式
-
需要控制子类的创建和初始化
-
需要包含状态(字段)
特点:
-
为相关类提供共同基类
-
可包含具体实现
-
支持模板方法模式
何时使用interface?
-
定义跨继承体系的能力
-
需要多重继承
-
实现松耦合设计
-
定义服务契约(如依赖注入)
-
支持测试替身(Mock/Stub)
特点:
-
定义契约/能力
-
支持多继承
-
实现解耦
简单梳理的口诀:
- 想做 "数据容器 + 简单方法"→ struct;
- 想做 "基类 + 子类复用逻辑"→ abstract class;
- 想做 "行为契约 + 解耦扩展"→ interface。