协程背后的核心原理:协程与线程到底是什么关系?

协程背后的核心原理:协程与线程到底是什么关系?

Unity的协程使用起来比较方便音效,但是由于它封装和隐藏了太多的细节,所以显得神秘。 比如协程真的是异步执行吗? 协程和线程之间有什么关系? 本文将从语义角度分析隐藏在协程背后的原理,并使用C++实现一个简单的协程,揭开协程的神秘面纱。 (文章中的代码为截图,可以点击放大。)

1.什么是协程?

简单来说,协程就是一个具有多个返回点的函数。 一般来说,一个函数只有一个返回点。 函数的调用者调用一次后,该函数的生命周期就结束了。 对于协程来说,它们的生命周期是由调用者决定的,可以通过返回值来决定如何进行调用以及如何结束调用。

由于协程返回一系列值3D角色,因此每个yield返回都对应一个return。 使用迭代器作为返回类型是更好的选择。 您可以简单地认为每个yield 返回都是针对迭代器中的一个元素。

Unity 的 C# 代码中协程的返回值通常是 IEnumerator 类型。 IEnumerator接口有两个方法,即Current和MoveNext。 我们可以简单地认为:协程是一个返回迭代器的函数。 最初,迭代器的 Current 指向函数的开头。 每次执行MoveNext时,Current都指向下一次yield返回的值。 例如:

#f:2:b:f:0:e:e:a:f:3:9:0:4:9:2:4:3:1:8:0:3:4:8:7:8:e:f:c:9:9:6:e#

2.协程的核心,神奇的yield

#b:6:d:1:a:a:e:3:0:7:1:6:5:5:2:c:6:1:9:3:d:e:9:0:1:e:6:e:f:3:c:5#

我们看一下上面代码示例中的函数协程:

上面代码的神奇部分是关键字yield。 为什么多次调用yield return可以返回一组值,并且函数的返回值变成了Enumerator类型? 这实际上是隐藏在协程背后的核心原理。 关于协程核心原理的分析和实现的文章有很多。 大多数都是从系统底层的角度来分析的。 使用汇编、goto语句或者C语言的setjmp、longjmp来实现协程。 虽然分析起来比较复杂。 内容比较透彻,但对读者知识要求较高,有些概念晦涩难懂。

其实从语义角度来看,协程的工作原理比较简单,任何支持闭包的语言都可以实现协程。 下面我们从语义角度解释一下协程:

1、将每个yield返回封装成一个函数(简称Y函数)。 如果函数使用外部局部变量,它将形成闭包。

2、Y函数的返回值是yield传入的对象和下一个yield返回封装的函数。

3.迭代器上调用MoveNext相当于调用Y函数

4、最后一个yield return封装的Y函数返回yield传入的对象和一个空函数,表示迭代结束(即调用MoveNext时返回false)

3.简单协程的C++实现

下面根据协程的语义解释,用C++实现一个简单的协程的例子:

#7:5:e:d:2:3:7:c:b:a:8:3:b:5:3:5:b:e:8:9:2:d:9:9:8:8:7:f:b:c:2:e#

#0:5:4:2:7:f:d:0:9:c:5:9:a:d:d:0:9:d:2:4:9:7:0:0:d:0:3:7:9:d:f:4#

一个简单的协程用不到 60 行代码实现。 对于上例中的协程函数,除了语法上有些差异外,语义与C#协程完全一致。 我们甚至可以引入一个宏unity 协程 实现睡眠,让它看起来更像一个 C# 协程,例如:

#3:5:c:3:1:7:6:1:9:5:c:d:6:d:1:9:6:9:6:b:1:8:f:2:9:3:b:9:d:b:b:0#

虽然上面的代码片段看起来更像是Unity的协程,但遗憾的是代码还不够简洁。 有一点语法噪音(函数末尾有一些无意义的括号)并且语法不够统一(最后一行是 return Yield)。

4.for循环

第三节我们讲了协程的简单实现,但是在for循环中使用yield还是有问题的,比如下面的代码段:

coroutine1函数返回的迭代器只包含值1,而不是我们期望的1~10。 因此,我们需要对协程中的循环进行特殊处理。 分析上面的代码unity 协程 实现睡眠,我们期望的是,对于循环中的每个i,调用yield生成一个协程节点,然后将这些节点合并成一个链表并返回。 因此,我们可以将协程中的for语句翻译为For函数。 代码如下:

#3:e:4:5:c:0:a:0:1:6:3:d:e:b:a:a:c:a:2:4:4:3:a:7:0:d:5:8:0:5:8:6#

组合函数实现了将两个协程节点连接在一起并将它们返回到一个链表中。 其定义代码如下:

#7:6:5:1:c:0:c:8:9:c:7:1:1:6:1:f:0:f:8:f:2:e:1:f:0:a:8:5:0:c:0:2#

使用 For 函数,coroutine1 实现可以更改如下:

#d:e:0:7:c:4:6:a:b:2:2:8:e:6:1:c:3:4:9:2:0:9:9:6:c:5:5:f:5:9:b:b#

虽然该函数在语法上有点难看,但它在语义上等价于以下 C# 代码:

#9:a:9:4:7:9:7:c:3:b:7:f:e:f:b:b:0:2:e:e:5:a:b:a:1:2:c:d:8:5:6:2#

这是一个更复杂的示例:

#5:c:1:e:9:9:6:c:b:e:7:d:c:7:2:3:f:3:e:1:f:c:7:c:4:1:3:4:d:9:e:0#

语义上等效的 C# 代码是:

#c:9:c:1:5:a:e:9:b:8:7:2:8:d:2:6:c:d:0:7:c:2:3:6:b:6:7:e:e:f:1:2#

5. 总结

本文从语义角度解释了协程的运行机制,并使用C++实现了一个简单的协程。 不过还有一些内容没有讨论到,比如For循环中如何实现break、yieldbreak等。 其实,要实施这些并不难。 有兴趣的同学可以自行研究。

回到引言中提到的两个问题:

协程是异步执行的吗? 严格来说,协程并不是异步执行的,但是调用者可以在时间片中执行每个yield,让程序看起来是异步的。

协程和线程之间有什么关系? 协程和线程之间没有严格的对应关系,但是可以将它们组合使用,隐藏不必要的细节以简化编程,例如Unity的WWW。

文章来源:http://mt.sohu.com/20170519/n493645931.shtml