协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈、局部变量和指令指针,同时又与其他协同程序共享全局变量和其他大部分东西。从概念上讲线程与协同程序的只要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作地运行。就是说,一个具有多个协同程序的程序在任意时刻只能运行一个协同程序,并且正在运行的协同程序只会在其显示地要求挂起时,它的执行才会暂停。
协同程序基础
Lua将所有关于协同程序的函数放置在一个名为“ coroutine”的table中。
函数create用于创建新的协同程序,它只有一个参数,就是一个函数。该函数的代码就是协同程序所需执行的内容。create会返回一个thread类型的值, 用以表示新的协同程序。通常create函数的参数是一个匿名函数,如:
co = coroutine.create(function () print("Hi") end)
print(co) -->thread:0x8071d98
一个协同程序可以处在4个不同的状态:挂起(suspended)、运行(running)、死亡(dead)和正常(normal)。当创建一个协同程序时,它处于挂起状态。也就是说 ,协同程序不会在创建它时自动执行其内容。可以通过函数status来检查协同程序的状态:
print(coroutine.status(co)) --suspended
函数coroutine.resume用于启动或再次启动一个协同程序的执行,并将其状态由挂起改为运行。
coroutine.resume(co) -->Hi
本例子中,协同程序的内容只是简单地打印了"Hi"后便终止了,然后它就处于死亡状态,也就再也无法返回了:
print(coroutine.status(co)) -->dead
到目前为止,协同程序看上去还只是像一种复杂的函数调用方法。其实协同程序真正的强大之处在于函数yield的使用上,该函数可以让一个运行中的协同程序挂起,而之后可以 再恢复它的运行。
co = coroutine.create(function ()
for i = 1, 10 do
print("co", i)
coroutine.yield()
end
end)
当唤醒这个协同程序时,它就会开始执行,直到第一个yield:
coroutine.resume(co) -->co 1
print(coroutine.status(co)) -->suspended
从协同程序的角度看,所有在它挂起时发生的活动都发生在yield调用中。当恢复协同程序的执行时,对于yield的调用才最终返回。然后协同程序继续它的执行,直到下一个yield调用或者执行结束:
coroutine.resume(co) -->co 2
coroutine.resume(co) -->co 3
coroutine.resume(co) -->co 4
...
coroutine.resume(co) -->co 10
coroutine.resume(co) -->什么都不打印
print(coroutine.status(co)) --dead
print(coroutine.resume(co))
-->false connot resume dead coroutine
注:resume是在保护模式中运行的。如果一个协同程序在执行中发生任何错误,Lua是不会显示错误消息的,而是将执行权返回给resume调用。
当一个协同程序A 唤醒另一个协同程序B 时,协同程序A就处于一个特殊状态,既不是挂起状态(无法继续执行A),也不是运行状态(是B在运行)。所以将这时A的状态称为“正常状态”。
Lua的协同程序还具有一项有用的机制,就是可以通过一对resume-yield来交换数据。在第一次调用resume时,并没有对应的yield在等待它,因此所有传递给resume的额外参数都将视为协同程序主函数的参数:
co = coroutine.create(function (a, b, c)
print("co", a, b, c) end)
coroutine.resume(co, 1, 2, 3) -->co 1 2 3
在resume调用的返回的内容中,第一个值为 true则表示没有错误,而后面所有的值都是对应yield传入的参数:
co = coroutine.create(function (a, b)
coroutine.yield(a + b, a - b)
end )
print(coroutine.resume(co, 20, 10)) -->true 30 10
与此对应的是,yield返回的额外值就是对应resume传入的参数:
co = coroutine.create(function ()
print("co", coroutine.yield())
end)
coroutine.resume(co)
coroutine.resume(co, 4, 5) -->co 4 5
最后,当一个协同程序结束时,它的主函数所返回的值都将作为对应resume的返回值:
co = coroutine.create(function ()
return 6, 7
end)
print(coroutine.resume(co)) -->true 6 7
Lua提供的是一种“非对称的协同程序”。也就是说,Lua提供了两个函数来控制协同程序的执行,一个用于挂起执行,另一个用于恢复执行。而一些其他的语言则提供了“对称的协同程序”,其中只有一个函数用于转让协同程序之间的执行权。
管道(pipe)与过滤器(filter)
生产者-消费者问题:
function producer ()
while true do
local x = io.read() --产生新值
send(x) --发送给消费者
end
end
function consumer ()
while true do
local x = receive() --从生产者接收值
io.write(x, "\n") --消费新值
end
end
如何将send与receive匹配起来。这是一个典型的“谁具有主循环(who-has-main-loop)”的问题。由于生产者和消费者都处于活动状态,它们各自具有一个主循环,并且都将对方视为一个可调用的服务。
协同程序被称为是一种匹配生产者和消费者的理想工具,一对resume-yield完全一改典型的调用者与被调用者之间的关系。当一个协同程序调用yield时,它不是进入了一个新的函数,而是从一个悬而未决的resume调用中返回。同样地,对于resume的调用也不会启动一个新函数,而是从一次yield调用中返回。这项特性正可用于匹配send和receive,这两者都认为自己是主动方,对方是被动方。receive唤醒生产者执行,促使其能产生一个新值。而send则 产出一个新值返还给消费者:
function receive ()
local status, value = coroutine.resume(producer)
return value
end
function send(x)
coroutine.yield(x)
end
因此,生产者一定是一个协同程序:
producer = coroutine.create(
function ()
while true do
local x = io.read()
send(x)
end
end)
在这种设计中,程序通过调用消费者启动。当消费者需要一个新值时,它唤醒生产者。生产者返回一个新值后停止运行,并等待消费者的再次唤醒。将这种设计称为“消费者驱动”。
可以扩展上述设计,实现“过滤器(filter)”。过滤器是一种位于生产者和消费者之间的处理功能,可用于对数据的一些变换。过滤器既是一个消费者又是一个生产者,它唤醒一个生产者促使其产生新值,然后又将变换后的值传递给消费者。
function receive (prod)
loacl status, value = coroutine.resume(prod)
return value
end
function send (x)
coroutine.yield(x)
end
function producer ()
return coroutine.create(
function ()
while true do
local x = io.read()
send(x)
end
end)
end
function filter (prod)
return coroutine.create(
function ()
for line = 1, math.huge do
local x = receive(prod)
x = string.format("%5d %s", line, x)
send(x)
end
end)
end
function consumer (prod)
while true do
local x = receive(prod)
io.write(x, "\n")
end
end
创建运行代码,将这些函数串联起来,然后启动消费者:
p = producer()
f = filter(p)
consumer(f)
或者:
consumer(filter(producer()))
以协同程序实现迭代器
将循环迭代器视为“生产者-消费者”模式的一种特例,一个迭代器会产出一些内容,而循环体则会消费这些内容。
例如:写一个迭代器,使其可以遍历某个数组的所有排列组合形式。若直接编写这种迭代器可能不太容易,但若编写一个递归函数来产生所有的排列组合则不会很困难。只要将每个数组元素都依次放到最后一个位置,然后递归地生成其余元素的排列。
function permgen (a, n)
n = n or #a --默认n为a的大小
if n <= 1 then
printResult(a)
else
for i = 1, n do
--将第i个元素放到数组末尾
a[n], a[i] = a[i], a[n]
--生成其余元素的排列
permgen(a, n - 1)
-- 恢复第i个元素
a[n], a[i] = a[i], a[n]
end
end
end
打印函数定义:
function printResult (a)
for i = 1, #a do
io.write(a[i], " ")
end
io.write("\n")
end
调用pergmen:
pergmen({1, 2, 3, 4})
-->2, 3, 4, 1
-->3, 2, 4, 1
-->3, 4, 2, 1
....
-->1, 2, 3, 4
协同程序实现迭代器:
首先将permgen中的printResult改为yield:
function permgen (a, n)
n = n or #a --默认n为a的大小
if n <= 1 then
coroutine.yield(a)
else
for i = 1, n do
--将第i个元素放到数组末尾
a[n], a[i] = a[i], a[n]
--生成其余元素的排列
permgen(a, n - 1)
-- 恢复第i个元素
a[n], a[i] = a[i], a[n]
end
end
end
再定义一个工厂函数,用于将生成函数放到一个协同程序中运行,并创建迭代器函数。迭代器只是简单地唤醒协同程序,让其产生下一种序列:
function permutations (a)
local co = coroutine.create(function () permgen(a) end)
return function () --迭代器
local code, res = coroutine.resume(co)
return res
end
end
for p in permutations {1, 2, 3, 4} do
printResult(p)
end
permutations函数使用了一种在Lua中比较常见的模式,就是将一条唤醒协同程序的调用包装在一个函数中。由于这种模式比较常见,所以Lua专门提供了一个函数coroutine.wrap来完成这个功能。类似于create,wrap创建了一个新的协同程序。但不同的是,wrap并不是返回协同程序本身,而是返回一个函数。每当调用这个函数,即可唤醒一次 协同程序。但这个函数与resume的不同之处在于,它不会返回错误代码。当遇到错误时,它会引发错误。
function permutations (a)
return coroutine.wrap(function () permgen(a) end)
end
非抢先式的多线程
协同程序提供了一种协作式的多线程。每个协同程序都等于是一个多线程。一对yield-resume可以将执行权在不同线程间切换。然而协同程序与常规的多线程的不同之处在于,协同程序是非抢先式的。当一个协同程序运行时,是无法从外部停止它的。只有当协同程序显示地要求挂起时(调用yield),它才会停止。
对于非抢先式的多线程来说,只要有一个线程调用了一个阻塞的操作,这个程序在该操作完成前,都会停止下来。对于大多数应用程序来说,这种行为是无法接受的。这也导致了许多程序员放弃协同程序,转而使用传统的多线程。接下来会用一个有用的方法来解决这个问题。
例如:从网站下载一个文件
require "socket"
host = "www.w3.org"
file = "/TR/REC-html32.html"
c = assert(socket.connect(host, 80))
c:send("GET " .. file .. " HTTP/1.0\r\n\r\n")
while true do
local s, status, partial = c:receive(2^10)
io.write(s or partial)
if status == "closed" then break end
end
c:close()
同时能够下载多个文件的例子:
function download (host, file)
c = assert(socket.connect(host, 80))
local count = 0
c:send("GET " .. file .. " HTTP/1.0\r\n\r\n")
while true do
local s, status, partial = receive(c)
count = count + #(s or partial)
if status == "closed" then break end
end
c:close()
print(file, count)
end
function receive (connection)
return connect:receive(2^10)
end
在并发的实现中,这个函数在接收数据时绝对不能阻塞。因此,它需要在没有足够的可用数据时挂起执行。修改如下:
function receive (connection)
connection:settimeout(0) --非阻塞调用
local s, status, partial = connection:receive(2^10)
if status == "timeout" then
coroutine.yield(connection)
end
return s or partial, status
end
调度程序:
threads = {} --用于记录所有正在运行的线程
function get (host, file)
--创建协同程序
local co = coroutine.create(function ()
download(host, file) end)
--将其插入记录表中
table.insert(threads, co)
end
function dispatch ()
--主调度
local i = 1
while true do
if threads[i] == nil then --还有线程吗
if thread[1] == nil then break end --列表是否为空
i = 1 --重新开始循环
end
local status, res = coroutine.resume(threads[i])
if not res then --线程是否完成任务了
table.remove(threads[i])
else
i = i + 1
end
end
end
main程序如下:
host = "www.w3.org"
get (host, "/TR/html401/html40.txt")
get (host, "/TR/2002/REC-xhtml1-20020801/xhtml.pdf")
...
dispatch() --主循环
一个问题:如果所有线程都没有数据可读,调度程序就会执行一个“忙碌等待”,不断地从一个线程切换到另一个线程,仅仅是为了检测是否有数据可读。
修改此问题:LuaSocket中的select函数,用于等待一组socket的状态改变,在等待时程序陷入阻塞状态。
function dispatch ()
--主调度
local i = 1
local connections = {}
while true do
if threads[i] == nil then --还有线程吗
if thread[1] == nil then break end --列表是否为空
i = 1 --重新开始循环
connections = {}
end
local status, res = coroutine.resume(threads[i])
if not res then --线程是否完成任务了
table.remove(threads[i])
else
i = i + 1
connections[#connections + 1] = res
if #connections == #threads then
socket.select(connections) --所有线程都阻塞
end
end
end
end