作者littleshan (我要加入剑道社!)
看板GameDesign
标题[程式] Coroutine: 入门篇
时间Thu Jun 28 23:19:42 2012
图文版
http://goo.gl/01f0e
很久以前就想要写一篇介绍 coroutine 的文章。Coroutine 对於游戏程式来说
是个非常有用的语言功能,不过学校课程几乎不会介绍这个东西。最近藉由实作
上的经验,整理出几篇心得,请各位慢慢品尝。
* * * * * *
Coroutine 一般翻译作「协程」,对一般 programmer 来说可能有些陌生,但其
实这个想法早在 1963 年就被提出来了。近年来因为 multi-threading 的普及,
主流语言鲜少提供 coroutine 的功能,但即使 coroutine 与 thread 的概念相
似,却有许多截然不同的特性,不少场合使用 coroutine 既能优雅地解决问题,
又能避免 multi-threading 的 race condition。本系列文章将会介绍 coroutine
的基本概念及应用场合,让原本复杂的流程变得简洁易懂。
Lua 是支援 coroutine 的语言之一,同时也具备简单易学的特性,以下将会使用
Lua 程式码作为 coroutine 的示范。对 Lua 不熟悉的读者可以参考 Lua Tutorial
Dictionary 上的教学,实际学起来是很快的。
[Lua Tutorial Dictionary]:
http://lua-users.org/wiki/TutorialDirectory
Coroutine 基本概念
Coroutine 可以视为「可以中断及继续执行的函式呼叫」。在一般的程式语言中,
呼叫某个函式时,该函式一定是从头开始执行:
function foo(n)
print(
"hello")
print(
"world")
print(
"n^2 = "..(n*n))
end
foo(
10)
-- 不间断地印出三行字
然而 coroutine 允许函式执行到一半就中断(yield),中断时内部状态会被保
留下来,呼叫端可以随时在之後恢复(resume)这个 coroutine。
function bar(n)
print(
"hello")
print(
"world")
coroutine.yield()
-- 在这里中断 coroutine
print(
"n^2 = "..(n*n))
end
local co =
coroutine.create(bar)
-- co 是保留 coroutine 内部状态的变数
coroutine.resume(co,
10)
-- 只印出前两行 hello world
print(
"this is the third line.")
-- 中断後主程式可以做其它事
coroutine.resume(co)
-- 印出最後一行 n^2 = 100
上述的范例即为 coroutine 的呼叫方法:
1. 呼叫端并非直接呼叫函式,而是使用 coroutine.create 来产生一个
coroutine object。这个 object 将会储存 coroutine 的执行状态,
包括区域变数的内容及中断点的位置。
2. 产生 coroutine object 的时候并不会呼叫函式,而是在第一次使用
coroutine.resume 的时候,才会从函式的开头开始执行。
3. 在函式中使用 coroutine.yield 时,将会中断函式执行并保留中断时
的状态,控制权随即转移到呼叫端。
4. 呼叫端再次执行 coroutine.resume 的时候,流程将会回到 coroutine
上次中断之处,继续往下执行到 yield 或函式结束。
任意中断的特性
Coroutine 中的两项操作:yield 及 resume,在 Lua 中只是基本的函式呼叫。
你可以使用任意数量的 yield 来中断 coroutine,当然也可以放在回圈之类的
控制结构当中。
function fibb()
local a =
1
local b =
0
while true do
print(a)
coroutine.yield()
local tmp = a
a = a + b
b = tmp
end
end
local co =
coroutine.create(fibb)
for i=
1,
20 do -- 印出费氏数列前20项
coroutine.resume(co)
end
当然,在 coroutine 中可以呼叫其它函式,而 yield 也能出现在更深层的函式
当中。我们会在下一篇文章中介绍这个应用。
Coroutine 的资料传递
在上面费氏数列的例子中,我们都是把数字印出来後才呼叫 coroutine.yield,
这显然弹性不够--呼叫端可能想把数字写到档案、或是塞进阵列中。我们能直
接把数值在 yield 的同时直接传回到主程式吗?答案是肯定的,而且方法非常
简单:直接当作参数丢给 coroutine.yield。
function fibb()
local a =
1
local b =
0
while true do
coroutine.yield(a)
local tmp = a
a = a + b
b = tmp
end
end
local co =
coroutine.create(fibb)
local array =
{} -- 建立空阵列
local state
-- 表示 coroutine 的状态
for i=
1,
20 do -- 把费氏数列前20项填进阵列中
state, array[i] =
coroutine.resume(co)
end
coroutine.resume 的第一个回传值皆为 true 或 false,表示该 coroutine 是否
正确执行。而第二个以後的回传值则是 coroutine 在中断时传进 coroutine.yield
的参数,因此在第 15 行我们需要用两个变数来接 coroutine 的回传值。
反过来说,主程式也可以在呼叫 coroutine.resume 的时候添加参数,这些参数会
被传递到 coroutine 当中,作为 coroutine.yield 的回传值。
function foo(n)
print(
"n = " .. n)
while true do
print(
"received " ..
coroutine.yield())
end
end
local co =
coroutine.create(foo)
coroutine.resume(co,
10)
-- 印出 n = 10
coroutine.resume(co,
20)
-- 印出 received 20
coroutine.resume(co,
30)
-- 印出 received 30
coroutine.resume(co,
40)
-- 印出 received 40
第一次呼叫 coroutine.resume 时,添加的参数会被当作是函式的参数,也就是 n。
第二次以後的 resume 其参数则会被当作 yield 的回传值送进 coroutine 之中。
籍由资料传递的功能,我们可以利用 coroutine 轻松实作出 iterator pattern。
我会在下一篇文章介绍这项应用。
Coroutine 与 Generator
严格说来,Lua 所提供的 coroutine 应该要称之为 generator。根据 wikipedia
的说法,coroutine 在进行 yield 操作时需指定另一个 coroutine 作为参数,
因此多个 coroutine 之间可以随意跳来跳去。而在 Lua 的 coroutine 中,yield
必然回到上次呼叫 resume 的地方。
这是 wikipedia 上的例子,注意 yield 时要同时指定流程跳跃的目地。
var q :=
new queue
coroutine produce
loop
while q is not full
create some new items
add the items to q
yield to consume
coroutine consume
loop
while q is not empty
remove some items from q
use the items
yield to produce
严格定义下的 coroutine 实际上可以用 generator 来达成,而许多语言似乎也
没有很精确地区分这两者。在接下来的文章中,我会继续用 coroutine 来称呼。
Coroutine 与 Thread
在系统层面上,coroutine 通常和 fiber 是一样的意思。Fiber 是一种特别的
user thread,如同一般的 thread 那样具有自己的 call stack 与 program
counter,但具备了以下的特性:
* Thread 不需自己定义中断点(也就是呼叫 yield 的地方),而是让 OS 或
thread library 来决定是否进行 context switch,通常由执行时间来判断。
然而 fiber 需要自行定义中断点,context switch 只会发生在明确呼叫
yield 的地方。
* 因为 context switch 是由使用者自行控制,因此 fiber 通常不需要 mutex
之类的东西来避免 race condition。
* Kernel thread 会进入 OS 的排程中,在多核心的 CPU 上可能会使用不同的
核心同时执行许多 kernel thread。但 fiber 属於特别种类的 user thread,
它无法利用多核心进行平行处理。
* * * * * *
下一篇文章中,我将以 iterator 作为例子,介绍简单的 coroutine 应用方式。
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 202.39.238.241
1F:推 s3748679:支持~ 加油~ -w-" 06/28 23:24
2F:推 adxis:GJ! 06/29 00:32
3F:推 gpmm:push 06/29 00:57
※ 编辑: littleshan 来自: 202.39.238.241 (07/08 17:54)
4F:推 wangm4a1:push 07/16 22:58
5F:推 SamMa: 感谢分享 受益良多 08/12 14:17