文章目录
- 协程
- 协程的定义和调度
- wrap
- Status
- Running
- 补充
- 协程与主程的数据交互——return...yield
协程
协程是一种并发操作,相比于线程,线程在执行时往往是并行的,并且线程在创建销毁执行时极其消耗资源,并且过长的执行时间会造成主进程阻塞。而协程可以以并发时轮值时间片来执行,优点是不会阻塞,消耗资源少,可以手动控制。至于协程和线程的区别,什么是并发并行,请自行查阅或者学习操作系统理论知识。
协程的定义和调度
在Unity中,由于游戏是单线程的(这是为了其他线程不阻塞进程),因此我们常常需要使用协程。在Lua中也支持协程,例如下列代码:
Corou1 =coroutine.create(function ()
print("我是协程1")
end)
print(Corou1)
Corou2 = coroutine.wrap(function ()
print("我是协程2")
end)
print(Corou2)
coroutine.resume(Corou1) --开始或继续协程
Corou2() --运行函数
输出:
thread: 00CAEC00
function: 00CA0578
我是协程1
我是协程2
我们可以用两种不同的方法创建协程,不过返回值一个是协程(虽然thread是线程),一个是函数。所以对这两个协程的运行方式也不同。如何理解这两种方式?我想用C#的例子:
void StartCorou() --function类型
{
StartCoroutine(Coroutine()); --thread类型直接执行
}
IEnumerator Coroutine()
{
}
thread类型定义的就是协程本身,因此我们可以用lua提供的协程table中的方法来调用,而function类型更像是定义了一个启动函数,通过调用这个函数来执行。
在Lua中的协程有一个优点:我们可以手动执行
Corou1 =coroutine.create(function ()
local i = 1
while true do
print(i)
i = i + 1
coroutine.yield(i) --yield也可以返回
end
end)
coroutine.resume(Corou1)
a,b = coroutine.resume(Corou1)
print(a,b)
coroutine.resume(Corou1)
输出:
1
2
true 3
3
通过注释我们可以发现,resume第一个返回固定为成功判断,然后返回yield提供的变长参数
function coroutine.resume(co: thread, val1?: any, ...any)
-> success: boolean
2. ...any
可以看到,对于进入了yield的协程,需要我们手动执行resume才能继续执行。而非自动地轮换时间片。这样有利有弊,但我认为在调度上更加灵活了。
wrap
使用wrap的话:
function f1()
local i = 1
while true do
print(i)
i = i + 1
coroutine.yield(i)
end
end
b = coroutine.wrap(f1)
print("返回值"..b())
print("返回值"..b())
print("返回值"..b())
输出:
1
返回值2
2
返回值3
3
返回值4
function coroutine.wrap(f: fun(...any):...unknown)
-> fun(...any):...unknown
使用wrap则我们发现,wrap并不返回成功判断,而是直接返回yield给出的变长参数,这是由于其内定义的函数就只接收变长参数。
根据菜鸟教程的描述,使用wrap将创建一个协程包装器,并将协同程序函数转换为一个可直接调用的函数对象
-- 使用 coroutine.wrap 创建了一个协同程序包装器,将协同程序函数转换为一个可直接调用的函数对象
co = coroutine.wrap(
function(i)
print(i);
end
)
co(1) --输出1
Status
Lua中的协程总共有四种状态:
function coroutine.status(co: thread)
-> "dead"|"normal"|"running"|"suspended"
以字符串形式返回协程 co 的状态。
return #1:
| "running" 正在运行。
| "suspended" 挂起或是还没有开始运行。
| "normal" 是活动的,但并不在运行。
| "dead" 运行完主体函数或因错误停止。
让我们从下面的例子看看协程什么时候会进入这些状态:
C2 = coroutine.create(function ()
print("协程2执行")
print("此时协程1:"..coroutine.status(Corou1))
coroutine.yield()
end)
Corou1 =coroutine.create(function ()
local i = 1
while i<3 do
print(i)
i = i + 1
if i==2 then
coroutine.resume(C2)
end
print(coroutine.status(Corou1))
coroutine.yield()
end
end)
print(coroutine.status(Corou1))
print("######")
coroutine.resume(Corou1)
print(coroutine.status(Corou1))
print("######")
coroutine.resume(Corou1)
print(coroutine.status(Corou1))
print("######")
coroutine.resume(Corou1)
print(coroutine.status(Corou1))
print("######")
输出:
suspended
######
1
协程2执行
此时协程1:normal
running
suspended
######
2
running
suspended
######
dead
######
从上述例子中看到,当协程未启动以及暂停中等待下一次resume时,其状态是suspended。而当协程正在执行时状态是running,当协程1执行切换到协程2时,协程1的状态是normal。最后当协程1退出时其状态是dead。
Running
running函数可以得到当前正在运行的协程(的地址)。
function coroutine.running()
-> running: thread
2. ismain: boolean
返回当前正在运行的协程加一个布尔量。 如果当前运行的协程是主线程,其为真。
(我试了一下没接收到这个bool,不知道什么情况)
用例:
Corou1 =coroutine.create(function ()
local i = 1
while i<3 do
print(i)
i = i + 1
print("协程1状态:"..coroutine.status(Corou1))
print(coroutine.running())
coroutine.yield()
end
end)
coroutine.resume(Corou1)
输出:
1
协程1状态:running
thread: 00A7E1F8 <-- 协程1的地址
先抛出一个问题:如果在上述的同样代码中,执行协程2时print这个running函数,请问可以得到什么?考虑到协程1在协程2运行时是normal状态,当前运行(running)的协程应当是协程2
C2 = coroutine.create(function ()
print("协程2执行")
print(coroutine.running())
print("此时协程1:" .. coroutine.status(Corou1))
print("协程2执行结束")
coroutine.yield()
end)
Corou1 =coroutine.create(function ()
local i = 1
while i<3 do
print(i)
i = i + 1
if i==2 then
coroutine.resume(C2)
end
print("协程1状态:"..coroutine.status(Corou1))
print(coroutine.running())
coroutine.yield()
end
end)
coroutine.resume(Corou1)
coroutine.resume(Corou1)
输出:
1
协程2执行
thread: 00C4CFE8
此时协程1:normal
协程2执行结束
协程1状态:running
thread: 00C4D408
2
协程1状态:running
thread: 00C4D408
补充
在协程第一次被执行之后,当第二次执行该协程时不需要输入所需的参数,例如下面的例子
Corou1 =coroutine.create(function (a,b)
coroutine.yield(a + b, a - b)
return a*b,a/b
end)
r1, r2, r3 = coroutine.resume(Corou1,20,10)
print(r1, r2, r3)
r1, r2, r3 = coroutine.resume(Corou1)
print(r1, r2, r3)
r1, r2, r3 = coroutine.resume(Corou1,20,10)
print(r1, r2, r3)
输出:
true 30 10 --执行yield
true 200 2.0 --执行return
false cannot resume dead coroutine nil
此外从上例也可以看出,执行协程也可以以return作为函数最后的返回值。而且一个已经dead的协程是不会被resume执行的。如果还需要调用这个已经死掉的协程,请重新创建一个,否则报错。
协程与主程的数据交互——return…yield
综合以上知识,我们看看菜鸟教程里的这个例子:
function foo (a)
print("foo 函数输出", a)
return coroutine.yield(2 * a) -- 返回 2*a 的值
end
co = coroutine.create(function (a , b)
print("第一次协同程序执行输出", a, b) -- co-body 1 10
local r = foo(a + 1)
return "协程结束"
end)
print(coroutine.resume(co, 1, 10)) -- true, 4
print(coroutine.resume(co)) -- true, 协程结束
输出:
第一次协同程序执行输出 1 10
foo 函数输出 2
true 4
true 协程结束
在上例中,协程co的语句中并无yield返回,而是将其放在foo函数之中,让函数执行完毕时return yield。所以这个协程在r=foo(a+1)执行完毕后就suspend了。那么究竟执行顺序是如何呢?
function foo (a)
print("foo 函数输出", a)
return coroutine.yield(2 * a) -- 返回 2*a 的值
end
co = coroutine.create(function (a , b)
print("第一次协同程序执行输出", a, b) -- co-body 1 10
local r = foo(a + 1) --协程停止
print("第二次协同程序执行输出", r)
local r, s = coroutine.yield(a + b, a - b) -- a,b的值为第一次调用协同程序时传入 --resume
print("第三次协同程序执行输出", r, s)
return b, "结束协同程序" -- b的值为第二次调用协同程序时传入
end)
print("main", coroutine.resume(co, 1, 10)) -- true, 4
print("--分割线----")
print("main", coroutine.resume(co, "r")) -- true 11 -9
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine
print("---分割线---")
第一次协同程序执行输出 1 10
foo 函数输出 2
main true 4
--分割线----
第二次协同程序执行输出 r
main true 11 -9
---分割线---
第三次协同程序执行输出 x y
main true 10 结束协同程序
---分割线---
main false cannot resume dead coroutine
---分割线---
上面的例子更复杂了,而且出现了一个我没意料到的情况:
第二次协同程序执行输出 r
在第一次协程执行时,r应当等于4,而现在我们将输入的字符“r”赋值给了它,这就意味着第二次执行协程的时候,第一次的r很可能才被我们的入参"r"赋值,让我们验证一下:
function foo (a)
print("foo 函数输出", a)
return coroutine.yield(2 * a) -- 返回 2*a 的值
end
co = coroutine.create(function (a , b)
print("第一次协同程序执行输出", a, b) -- co-body 1 10
local r = foo(a + 1) --去掉local声明不影响结果,不是local的原因
print("第二次协同程序执行输出", r)
end)
print("main", coroutine.resume(co, 1, 10)) -- true, 4
print("--分割线----")
print("main", coroutine.resume(co))
输出:
第一次协同程序执行输出 1 10
foo 函数输出 2
main true 4
--分割线----
第二次协同程序执行输出 nil
在协程进入suspend之后,这回我们不传入参数,输出结果是nil而非4。
也就是说return coroutine.yield(2 * a)
语句,并不会将yield内的内容返回给return,而且它们的执行顺序是先执行coroutine.yield
后执行return
。其流程是:coroutine.yield
-> 等待resume
->resume回传入参给yield
->return 执行,返回这个入参
,传入多个参数验证:
function foo (a)
print("foo 函数输出", a)
return coroutine.yield(2 * a) -- 返回 2*a 的值
end
co = coroutine.create(function (a , b)
print("第一次协同程序执行输出", a, b) -- co-body 1 10
local r,v = foo(a + 1)
print("r",r,"v",v)
end)
print(coroutine.resume(co, 1, 10)) -- true, 4
print(coroutine.resume(co,5,20))
输出:
第一次协同程序执行输出 1 10
foo 函数输出 2
true 4
r 5 v 20
true
因此,这意味着协程中的接收return...yield
的变量,在yield之后其返回值实际上是我们resume再回传回去的,而回传的参数将被return接收,所以return...yield
都需要我们手动进行定义/传参,现在我能理解之前的例子了:
function foo (a)
print("foo 函数输出", a)
return coroutine.yield(2 * a) -- 返回 2*a 的值
end
co = coroutine.create(function (a , b)
print("第一次协同程序执行输出", a, b) -- co-body 1 10
local r = foo(a + 1) --协程停止
print("第二次协同程序执行输出", r) --r需要传参
r, s = coroutine.yield(a + b, a - b) -- a,b的值为第一次调用协同程序时传入
print("第三次协同程序执行输出", r, s)
return b, "结束协同程序" -- b的值为第二次调用协同程序时传入
end)
print("main", coroutine.resume(co, 1, 10)) -- true, 4
print("--分割线----")
print("main", coroutine.resume(co, "r")) -- true 11 -9,"r"传入resume后的第一个变量r
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end,"x","y"分别传入r,s
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine
print("---分割线---")
输出:
第一次协同程序执行输出 1 10
foo 函数输出 2
main true 4
--分割线----
第二次协同程序执行输出 r
main true 11 -9
---分割线---
第三次协同程序执行输出 x y
main true 10 结束协同程序
---分割线---
main false cannot resume dead coroutine
---分割线---
所以对应的,resume
的第一次传入参数是作为协程开启的函数入参,而之后的resume
的传入参数将被yield
接收。