协程(Coroutine)原理分析

时间:2021-06-08 20:04:03

前言

又是小半个月没更新了,感觉内心十分的愧疚,毕竟说过的话一般还是要去做到的,一般吧。。这次闲的没事,就来扒一扒Unity一个你经常会在别人的代码里见到,但可能从来都没有用过的语法,没错,它就是传说中的————协程(Coroutine)。

什么是协程?

首先来看一下Unity官方文档的定义:

A coroutine is a function that can suspend its execution (yield) until the given given YieldInstruction finishes.

嗯,大概翻译一下就是: 协程是一种可以暂停执行过程的函数,它可以中断当前的执行过程直到下一个Yield指令达成。如果要做个类比的话,你可以把它理解为类似于CPU在多个进程间切换,从而达到多个进程同时执行的效果。学过计算机组成原理的都知道,当CPU在多个进程间切换时,那些后台程序就会处于这种暂停用英文的Suspend或许更恰当)的状态,所以早年的电脑即使用一个CPU也可以同时处理多个进程任务,这是一种“伪多线程”的技术。记住,这里要划一个重点,协程是一种“伪多线程”,明白这一点你理解协程会轻松很多。

协程函数的标准写法

孔乙己有一句著名的话很多人估计都学过————“你晓得茴字有八种写法么?”庆幸的是协程只有一种写法,但是不要因为它被称作“函数”就放松警惕。相反,把它和函数说成是两家或许更恰当。下面是一段标准的Coroutine代码:

IEnumerator WaitAndPrint(float waitTime)
{
while (true)
{
yield return new WaitForSeconds(waitTime);
print("WaitAndPrint " + Time.time);
}
}

从第一句话我们就可以看出它和函数的不同————协程是没有返回值的,相反它必须用一个关键词来修饰,那就是IEnumerator。如果你去查IEnumerator的语义,会发现它是C#库里用来声明迭代器的一种语法。但是在Unity里这么用,已经超出了它原本的语义,所以你大可不必去研究它的起源,只用单纯的告诉自己:IEnumerator在Unity里是协程的标志

或许你会疑惑不解:既然协程没有返回值,那么它为什么还需要return呢?没错,这就是协程和函数的第二点不同————协程用yield return代替return,并且至少要有一个yield。如果你手贱又去查了一下词典,可能会被yield的“八种写法”彻底搞蒙,我可以从百度翻译截个图你自己感受下:
协程(Coroutine)原理分析
我可以很明确地告诉你,它的所有释义里没有一个能用的。。。非要说的话,可能“退让”还挨点边,不过你还不如直接把它理解为suspend更贴切一点。之所以说协程里必须要有至少一个yield语句,是因为协程的定义就是“可暂停的函数”。一个断点都不设,它就和外面的妖艳贱货一样了不是。所以当你在考虑是不是需要使用协程时,一个重要的衡量依据就是“我需不需要让它暂停执行,直到某个条件满足才会继续”。

说完协程的两大特色之后,我们就可以简单地解释一下上面的例子到底干了什么。上述的例子使用一个无限循环的while语句在每隔给定的waitTime时间后打印一个等待时间的Log(print用于向控制台输出日志,等价于Log.Debug)。协程的基本语法和函数基本类似,所以除了需要特别注意的几点之外,我们完全可以以相同的控制结构编写协程。

协程的正确调用方法

除了声明方式独具一格之外,协程的调用也和函数不尽相同。想要在正常的函数里调用协程,必须在像调用正常函数那样的语法外面套上一层StartCoroutine, 它的详细语法更像是下面这样

void Start()
{
// - After 0 seconds, prints "Starting 0.0"
// - After 0 seconds, prints "Before WaitAndPrint Finishes 0.0"
// - After 2 seconds, prints "WaitAndPrint 2.0"
print("Starting " + Time.time);


// Start function WaitAndPrint as a coroutine.

coroutine = WaitAndPrint(2.0f);

StartCoroutine(coroutine);

print("Before WaitAndPrint Finishes " + Time.time);
}

这里又有一个可能产生疑惑的点: StartRoutine接受的是一个协程,返回的又是另一个协程,它们两者有什么区别? 正确的答案应该是————StartRoutine接受的是一个迭代器(你完全可以把它理解为协程函数),返回的则是一个Coroutine对象。是不是感觉这个突然冒出来的Coroutine类很诡异?其实你大可不必担心,因为你根本用!不!上!不相信?那你可以看一看Unity的官方文档:

MonoBehaviour.StartCoroutine returns a Coroutine. Instances of this class are only used to reference these coroutines and do not hold any exposed properties or functions.

A coroutine is a function that can suspend its execution (yield) until the given given YieldInstruction finishes.

大体意思就是调用StartCoroutine会返回一个Coroutine对象,但是Coroutine对象只是用来引用协程函数(这里使用小写的coroutine表示)的,不包含任何可供外界访问的属性或函数。说的好听点,就是两个字————“花瓶”,用来给你看的,你从它那得不到任何有用的货,想知道它干了啥还得看它引用的协程函数。以后默认情况下我们提到协程就等同于协程函数了。

我知道你很想骂街,觉得让StartCoroutine返回这么个东西是多此一举,其实Unity这么设计自有它的道理,想要搞清楚,还得看下面的一个小节。

YieldInstruction 汇总

还记得协程(函数)的定义吗?一个可以悬挂并在之后满足某个条件时继续执行的函数。当时我们没有明说的一点就是这里的“某个条件”到底包括了啥,现在你就可以听我娓娓道来。
首先还是祭出神器”Unity Script API”,即Unity官方文档,翻到YieldInstruction这里,你可以看到非常简单的两句话:

Base class for all yield instructions.

See WaitForSeconds, WaitForFixedUpdate, Coroutine and MonoBehaviour.StartCoroutine for more information.

没了,就这两句话,不过还有一个隐藏的信息我们需要知道的就是————YieldInstruction不派生自任何类,这意味着它和Object(UnityEngine中的)是同等地位的。YieldInstruction,正如它的名字一样,是所有“某个条件”的鼻祖,你可以设置的条件大部分都派生自该基类。没错,是“大部分”,而不是所有。WaitForSeconds和WaitForFixedUpdate就是它的两个子类,估计你看一眼它们的名字就知道了它们是用来干啥的,一个是让协程悬停固定的秒数,一个是悬停固定的帧数,没什么好讲的。有意思的是还有一个类也派生自它,那就是之前那个鸡肋的Coroutine。还记得上一节的内容嘛?Coroutine是一个花瓶类,只能通过StartCoroutine返回。哦呦呦,这就有意思了嘛,这就为我们提供了另一种可能性,那就是在函数里那个把无数人折磨得要死的特性————递归。有了Coroutine,我们就可以放心大胆地使用yiled return StartCoroutine(myCoroutine)这种自己写着很帅,看你代码的人很伤的炫技。所以当遇到你不懂的东西时,不要总想着这个东西有什么用,而应该想着我能怎么去用。

Beyond YieldInstruction

上一节说了一个很重要的观点:YieldInstruction可以设置“部分”悬停条件,但不是所有的条件。那么所有的条件还包括哪些呢?其实常用的不外乎两个————yield nullyield WWW

yield null灰常简单,就是在下一帧所有的Update函数都执行完了之后再执行,类似但绝对不等于WaitForFixedUpdate(1)。关于二者的不同,不是本篇能一句两句说的玩的,以后还会继续在介绍Event Function的执行顺序时继续分析。

yield WWW 说起来就更是一段很长很长的故事了,你只需要知道它是用来在后台下载完成时才会继续的条件就可以了。如果你真的用到了它,那就去自行翻阅官方文档吧。。。

对了,如果你见过很多前辈的代码,可能还会遇到一种悬挂条件,那就是yield n,这里的n代表任意数字。这种语法本质上等价于yield null, 但是官方文档并没有解释这种语法,所以最好还是不要模仿,更不要把yield 3当成是悬挂三秒(帧),这是很容易出现的误读。

结语

本文大概介绍了协程的语法和用法,主要解释了协程的其中一个特性:可以悬停和恢复的函数。当然,协程还有另一个重要的性质没有介绍:协程是一个典型的“伪多线程”,属于Unity里的一种语言特性。关于这一点,我们还有很多可以聊的,但是篇幅限制,今天就到这里吧,回见各位~~