- 协程(Coroutine)是Unity中一个让代码异步执行的一种机制,协程之间是单线程执行的,同时他的特点是能让程序员用yield主动挂起,并且显式地控制唤醒的时机。
- 通过协程可以实现计时器、将耗时的操作拆分成几个步骤分散在每一帧去运行等等。
- 协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈,局部变量和指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。
- 线程与协同程序的主要区别在于,一个具有多线程的程序可以同时运行几个线程,而协同程序却需要彼此协作地运行。
- 就是说,一个具有多个协同程序的程序在任何时刻只能运行一 个协同程序,并且正在运行的协同程序只会在其显示地挂起时,它的执行才会暂停。
协程的使用
- StartCoroutine:开始协程
- StopCoroutine:停止协程
- 协程中要以IEnumerator为返回值,通过yield来暂停协程的执行,同时说明下次开启的条件
- 协同程序主要是在Update()方法之后,LateUpdate()方法之前调用。
- 协程停止和创建必须要用同样的形式来进行
- 参数中写字符串的形式
- 参数传IEnumerater类型对象的形式
- 参数传Coroutine类型对象的形式
- Start可以为IEnumerator类型,成为协程的形式,然后可以在start中yield等待任何一个IEnumerator为返回值的函数执行
协程、Invoke、InvokeRepeating的执行区别
实验1:禁用对应脚本
Coroutine:执行
Invoke:执行
InvokeRepeating:执行
实验2:禁用对应的gameobject
Coroutine:不执行
Invoke:执行
InvokeRepeating:执行
实验3:销毁对应的脚本
Coroutine:不执行
Invoke:不执行
InvokeRepeating:不执行
实验4:销毁对应的gameobject
Coroutine:不执行
Invoke:不执行
InvokeRepeating:不执行
底层实现
- 协程其本质其实是通过IEnumerator迭代器实现的一种状态机,故其本质还是单线程的,一旦协程卡住整个线程也会卡住。
- Unity实际上是可以对所有的继承自YieldInstruction的类去使用协程的,也就是说Yied关键字对所有的YieldInstruction继承的类都是生效的。
- 关于C#迭代器的实现,可参考这个文章
- IEnumerator迭代器:
-
//迭代器接口 public interface IEnumerator { //Current属性为只读属性,返回枚举序列中的当前位的内容 object Current { get; } //MoveNext()把枚举器的位置前进到下一项,返回布尔值,新的位置若是有效的,返回true;否则返回false bool MoveNext(); //Reset()将位置重置为原始状态 void Reset(); }
-
//一个迭代器的例子 class B { public IEnumerator enumerableFuc() { int i = 0; Console.WriteLine("Enumerator " + i); yield return i; i++; Console.WriteLine("Enumerator " + i); yield return i; i++; Console.WriteLine("Enumerator " + i); yield break; } } class Program { static void Main(string[] args) { B b = new (); IEnumerator enumerator = b.enumerableFuc(); while (enumerator.MoveNext()) { Console.WriteLine("main " + enumerator.Current); Console.WriteLine("Enumerator Run"); } } }
- 以上的例子可以看出enumerableFuc方法被截成了3个部分,通过MoveNext()返回的bool来判断迭代器是否走完,而yield return的值则通过Current属性返回。
- 整个过程可以理解为当我们调用IEnumerator enumerator = b.enumerableFuc();时,enumerableFuc方法会根据yield关键字将整个方法拆分成各个代码块。
- yield关键字是一个语法糖,背后其实生成了一个新的枚举器类。
- yield return的作用是在执行到这行代码之后,将控制权立即交还给外部。
- yield return之后的代码会在外部代码再次调用MoveNext时才会执行,然后执行下一个yield return或是迭代结束。
- yield break语法块会直接返回一个MoveNext 返回值为false的枚举器类,从而终止迭代器运行。
-
自己实现一个协程
class CoroutinesManager
{
public List<IEnumerator> coroutines = new List<IEnumerator>();
//模仿协程开始,将协程加入到协程的执行列表中
public void StartCoroutine(IEnumerator coroutine)
{
coroutines.Add(coroutine);
}
}
class B
{
public IEnumerator enumerableFuc1()
{
int i = 0;
Console.WriteLine("Enumerator1 " + i);
yield return i;
i++;
Console.WriteLine("Enumerator1 " + i);
yield return i;
i++;
Console.WriteLine("Enumerator1 " + i);
yield break;
}
public IEnumerator enumerableFuc2()
{
int i = 100;
Console.WriteLine("Enumerator2 " + i);
yield return i;
i++;
Console.WriteLine("Enumerator2 " + i);
yield return i;
i++;
Console.WriteLine("Enumerator2 " + i);
yield break;
}
}
class Program
{
static void Main(string[] args)
{
int i = 0;
CoroutinesManager coroutinesManager = new CoroutinesManager();
B b = new();
coroutinesManager.StartCoroutine(b.enumerableFuc1());
coroutinesManager.StartCoroutine(b.enumerableFuc2());
//模仿Unity生命周期循环(一帧)
while (true)
{
Console.WriteLine("frame: " + ++i);
if (coroutinesManager.coroutines.Count <= 0)
{
return;
}
List<IEnumerator> delete = new List<IEnumerator>();
foreach (var item in coroutinesManager.coroutines)
{
if (!item.MoveNext())
{
delete.Add(item);
}
}
foreach (var item in delete)
{
coroutinesManager.coroutines.Remove(item);
}
}
}
}
注意:unity中的协程比我们实现的更加复杂,包括协程的嵌套、调度器,计时器实现、停止都没有实现。
- unity为了实现协程在干的事情其实就是支持了一堆yield return 的返回值, 再写一个调度器,根据返回值来按时候唤醒(调用.MoveNext())。
- 比如你yield return new WaitForSeconds(1), 在返回后调度器就每一帧来看一下,这个函数离上次yield的时候有1秒了没,如果到1秒了,就调用迭代器的.MoveNext()。
- 调度器的运行细节:
- 首先每次的StartCoroutine()其实就是在调度系统里“注册”了这个协程。
- 当你编写了一个带yield return的协程函数的时候,C#编译器会这个函数生成一个类。
- 当你发起一个协程的时候,就会生成一个Coroutine对象。
- 这个Coroutine对象用来保存追踪这个协程的执行状态,比如当前本地变量的值,yield的状态。
- 因此协程是会带来一定程度的内存开销以及GC。
- Unity协程中yield return支持的协程响应类有以下这些:
-
//下一帧执行后续代码 yield return null; //下一帧执行后续代码 yield return 数字; //结束迭代器 yield break; //Unity用于进行基于协程的异步操作的基类 yield return new AsyncOperation(); //协程的嵌套 yield return IEnumerator; //用于协程的嵌套和监听Coroutine类是StartCoroutine返回的协程句柄 yield return new Coroutine(); //WWW继承CustomYieldInstruction,CustomYieldInstruction可以用于实现自定义协程响应类 yield return new WWW(); //在这帧结束,在 Unity 渲染每一个摄像机和 GUI 之后,在屏幕上显示下一帧之前执行后续代码。 yield return new WaitForEndOfFrame(); //下一帧FixedUpdate开始时执行后续代码 yield return new WaitForFixedUpdate(); //延时设置的等待时间之后一帧的Update执行完成后运行,受到Time.timeScale影响 yield return new WaitForSeconds(等待时间/秒); //延时设置的等待时间之后一帧的Update执行完成后运行 yield return new WaitForSecondsRealtime(等待时间/秒);
-
协程与Invoke的区别
- Invoke函数必须输入方法名,对于上下文变量、属性的引用就会尤为不便,因此不能传有参方法进Invoke
- Invoke执行没有被挂起,相当于设置完被调用函数的执行时间后即时向下执行
- 协程新开一条执行序列(跟新建线程差不多)并挂起,等待中断指令结束
- 协程相比Invoke拥有更多参数选择,比如等待一帧后执行,等待某个函数执行完毕后执行等
- 协程的效率比Invoke高
协程的缺点
- 依赖于MonoBehaviour,比如我们在MVC开发时,如果M、C层不继承MonoBehaviour,也就无法使用协程
- 因为返回值为IEnumerator,所以不能有其他返回值
- 如果要顺序的完成一些任务,使用协程会造成回调地狱(回调函数里面嵌套回调函数)
- 如何避免:
- 自定义一个调度器(不依赖于MonoBehaviour)
- 使用异步Async/Await(避免回调地域)
- 使用UniTask来进行异步编程(详见后面的参考资料)
协程、进程、线程的区别
进程:
- 保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己独立的地址空间,有自己的堆,上级挂靠单位是操作系统。
- 一个应用程序相当于一个进程,操作系统会以进程为单位,分配系统资源(CPU 时间片、内存等资源),进程是资源分配的最小单位。
线程:
- 线程从属于进程,是程序的实际执行者。线程是操作系统能够进行运算调度的最小单位。
- 它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一个线程只有一个进程。
- 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。
协程:
- 协程是伴随着主线程一起运行的一段程序。意点:
- 协程与协程之间是并行执行,与主线程也是并行执行,同一时间只能执行一个协程提起协程,自然是要想到线程,因为协程的定义就是伴随主线程来运行的!
- 一个线程可以拥有多个协程,协程不是被操作系统内核所管理,而完全是由程序所控制。
- 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。