协同程序与线程差不多,拥有自己独立的栈、局部变量和指令指针,同时又与其它协同程序共享
全局变量和其它大部分东西。线程与协同程序的主要区别:
一个具有多个线程的程序可以同时运行几个线程;而一个具有多个协同程序的程序在任意时刻只
能运行一个协同程序,并且正在运行的协同程序只会在其显示地要求挂起时,它的执行才会暂停。
9.1 协同程序基础
Lua将所有协同程序的函数放置在一个名为"coroutine"的table中。
函数create用于创建新的协同程序,它只有一个参数,就是一个参数。该函数的代码就是
协同程序所需执行的内容。create会返回一个thread类型的值,用以表示新的协同程序。
通常create的参数是一个匿名函数。示例:
co = coroutine.create(function() print("hi")end)
print(co) --> thread:ox8071d98
一个协同程序可以处于4种不同的状态:
挂起(suspended)、运行(running)、死亡(dead)和正常(normal)。
当创建一个协同程序时,它处于挂起状态,而不会自动执行其内容。
函数status来检查协同程序的状态:
print(coroutine.status(co)) --> suspended
函数coroutine.resume用于启动或再次启动一个协同程序的执行,并将其状态由挂起改为
运行; coroutine.resume(co) --> hi
协同程序终止后就处于死亡状态,再也无法返回了:
print(coroutine.status(co)) --> dead
函数yield可以让一个运行的协同程序挂起,而之后可以再恢复它的运行。示例:
co = coroutine.create(function()
for i=1, 10do
print("co", i)
oroutine.yield()
end
end)
coroutine.resume(co) --> co 1
print(coroutine.status(co)) --> suspended
coroutine.resume(co) --> co 2
coroutine.resume(co) --> co 3
...
coroutine.resume(co) --> co 10
coroutine.resume(co) --> 什么都不打印
最后一次调用resume时,协同程序的内容已经执行完毕,并已经返回。因此,这是协同
程序处于死亡状态,如果试图再次恢复它的执行,resume将返回false及一条错误消息:
print(coroutine.resume(co)) --> false cannot resumedead coroutine
注意: resume是在保护模式中运行的。因此如果在一个协同程序的执行中发生任何错误,
Lua是不会显示错误消息的,二是将执行权返回给resume调用。
当一个协同程序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, 4, 5) --> co 4 5
最后当一个协同程序结束时,它的主函数所返回的值都将作为对应resume的返回值:
co = coroutine.create(function()
return 6, 7
end)
print(coroutine.resume(co)) --> true 6 7
注: 很少在一个协同程序中使用所有这些功能,但每种功能各有其用途。
Lua提供的协同程序是一种"非对称的协同程序(asymmetric coroutine)",也就是说Lua提供
了两个函数来控制协同程序的执行,一个用于挂起执行,另一个用于恢复执行。在这种实现
中,一个协同程序只能在它没有调用其它函数时(它的调用栈中没有未完成的调用),才可以
挂起执行。换句话说,只有协同程序的主函数才能调用类似于yield这样的函数。
9.2 管道(pipe)与过滤器(filter)
关于协同程序的经典示例就是"生产者-消费者"的问题。这其中涉及到两个函数。一个函数
不断地产生值,另一个则不断地消费这些值。
协同程序被称为是一种匹配生产者和消费者的理想工具,一对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)
在这种设计中,程序通过调用消费者来启动。当消费者和需要一个新值时,它唤醒生产者。
生产者返回一个新值后停止运行,并等待消费者的再次唤醒。这种设计被称为"消费者驱动
—— consumer-driven".
可扩展上述设计,实现过滤器(filter).过滤器是一种位于生产者和消费者之间的处理功能,
可用于对数据的一些变换。过滤器及时一个消费者又是一个生产者,它唤醒一个生产者
促使其产生新值,然后又将变换后的值传递给消费者。示例:在前面代码中添加一个过滤器,
在每行起始处插入一个行号——
function receive(prod)
local 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)
returncoroutine.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()))
9.3 以协同程序实现迭代器
将循环迭代器视为"生产者—消费者"模式的一种特例,一个迭代器会产出一些内容,而循环体
则会消费这些内容。因此,协同程序也适用于实现迭代器。
示例迭代器 —— 遍历某个数组的所有排列组合形式。
第一步:编写一个递归函数来产生所有的排列组合形式。想法 —— 将每个数组元素一次放到
最后一个位置,然后递归生成其余元素的排列。代码:
function permgen(a, n)
n = n or #a
if n <= 1then
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, #ado
io.write(a[i]," ")
end
io.write("\n")
end
调用:permgen({1,2,3,4})
第二步:函数完成后,将其转换为一个迭代器。
首先,将printResult改为yield
function permgen(a, n)
n = n or #a
if n <= 1then
coroutine.yeild(a)
else
<asbefore>
然后,定义一个工厂函数,用于将生成函数放到一个协同程序中运行,并创建迭代器函数。
迭代器只是简单地唤醒协同程序,让其产生下一种排列:
function permutations(a)
local co =coroutine.create(function() permgen(a) end)
returnfunction()
localcode, res = coroutine.resume(co)
returnres
end
end
最后,在for语句中遍历一个数组的的所有排列。
for p in permutations({"a", "b","c"}) do
printResult(p)
end
permutations函数使用了一种在Lua中比较常见的模式,就是将一条唤醒协同程序的调用
包装在一个函数中。了解coroutine.wrap
9.4 非抢先式的多线程
协同程序提供了一种协作式的多线程,每个协同程序都等于是一个线程。一对yield-resume
可以将执行权在不同线程之间切换。然而,协同程序与常规的多线程的不同之处在于,协同
程序时非抢先式的。就是说,当一个协同程序在运行时,是无法从外部停止它的。只有当协
同程序显示地要求挂起时(调用yield),它才会停止。
对于非抢先式的多线程来说,只要有一个线程调用了一个阻塞的操作,整个程序在该操作
完成前,都会停止下来。对于很多应用程序来说,这种行为是无法接受的。解决方法,
见示例——希望通过HTTP下载几个远程文件。
下载一个文件的步骤:
第一步,加载LuaSocket库,require"socket"
第二步,定义主机和下载的文件。如
host="www.w3.org"
file = "/TR/REC-html32.html"
第三步,打开一个TCP连接,连接到该站点的80端口。
c = assert(socket.connect(host, 80))
这步操作返回一个连接对象,可以用他来发送文件请求。
c:send("GET" .. file .. "HTTP/1.0\r\n\r\n")
第四步,按1k的字节块来接收文件,并将每块写到标准输出。
while truedo
local s, status, partial = c:receive(2^10)
io.write(sorpartial)
if status =="closed" then break end
end
在正常情况下receive函数会返回一个字符串。若发生错误,则会返回nil, 并且附加错误
代码(status) 及出错前读取到的内容(partial).当主机关闭连接时,就将其余接收到的内容
打印出来,然后退出接收循环。
第五步,下载完文件后,关闭连接。c:close()
下载多个文件:
如果逐个下载文件,这种做法太慢。当接收一个远程文件时,程序将大部分时间花费在等待
数据接收上,更明确地说,是将时间用在了对receive的阻塞调用上。因此如果一个程序可以
同时下载所有文件的话,那么它的运行速度就快很多。想法 -- 为每个下载任务创建一个新的线程,
只要一个线程无可用数据,它就可以讲控制权转让给一个简单的调度程序,而这个调度程序则会
调用其它的下载线程。
将之前的下载代码封装为一个函数,代码如下:
function download(host, file)
local 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
辅助函数receive来从连接接收数据,在顺序下载的方法中receive的代码可以这样写:
function receive(connection)
returnconnection:receive(2^10)
end
在并发的实现中,这个函数在接收数据时绝对不能阻塞。它需要在没有足够的可用数据时挂起执行。
代码如下:
function receive(connection)
connection:settimeout(0) --使recevie调用不会阻塞
local s,status, partial = connection:receive(2^10)
if status =="timeout" then
coroutine.yield(connection)
end
return s orpartial, status
end
对settimeout(0)的调用可使以后所有对此连接进行的操作不会阻塞。若一个操作返回的status为timeout(超时),
就表示该操作在返回时还未完成。此时线程就会挂起执行。
调度程序及一些辅助代码。table thread 为调度程序保存着所有正在运行中的线程。函数get确保每个下载任务
都在一个独立的线程中执行。调度程序本身就是一个循环,它遍历所有的线程,逐个唤醒它们的执行。并且
当线程完成任务时,将该线程从列表中删除。在所有线程都完成运行后,停止循环。
thread = {}
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
ifthreads[1] == nil then break end
i = 1
end
localstatus, res = coroutine.resume(thread[i])
if notres then
table.remove(threads, i)
else
i = i + 1
end
end
end
最后主程序需要创建所有的线程,并调用调度程序。如:
host = "www.w3.org"
get(host, "/TR/html401/html40.txt")
get(host, "/TR/2002/REC-xhtml1-20020801/xhtml1.pdf")
get(host, "/TR/REC-html32.html")
get(host,"/TR/2000/REC-DOM-Level-2-Core-20001113/DCM2-Core.txt")
dispacth() -- 主循环
上述的的实现不够完美,如果所有线程都没有数据可读,调度程序就会执行一个"忙碌等待",
不断地从一个线程切换到另一个线程。这样便导致这个协同程序的实现会比顺序下载多耗费
30倍的CPU时间。解决方法是用LuaSocket中的select函数。这个函数可以用于等待一组socket
的状态改变,在等待时陷入阻塞状态。若要在当前实现中应用这个函数,只需修改调度程序即可。
新版本如下:
function dispatch()
local i = 1
local connections = {}
while true do
if threads[i] == nil then
if threads[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 == #thread then
socket.select(connections)
end
end
end
end