Lua 的所有协同函数存放于 coroutine table 中。create 函数用于创建新的协同程序,
其只有一个参数:一个函数,即协同程序将要运行的代码。若一切顺利,返回值为 thread类型,表示创建成功。通常情况下,create 的参数是匿名函数:
co = coroutine.create(function ()
print("hi")
end)
print(co)
--> thread: 0x8071d98
协同有三个状态:
当我们创建协同程序成功时,其为挂起态,即此时协同程序并未运行。我们可用 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 10
coroutine.resume(co)
-- prints nothing
上面最后一次调用时,协同体已结束,因此协同程序处于终止态。如果我们仍然希
望激活它,resume 将返回 false 和错误信息。
print(coroutine.resume(co))
--> false cannot resume dead coroutine
注意:resume 运行在保护模式下,因此,如果协同程序内部存在错误,Lua 并不会抛出错误,而是将错误返回给 resume 函数
。
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
第二个例子,数据由 yield 传给 resume。true 表明调用成功,true 之后的部分,即是
yield 的参数。
co = coroutine.create(function(a, b)
coroutine.yield(a + b, a - b)
end)
coroutine.resume(co,30,10) -- true 40 20
print(coroutine.status(co)) -- suspended
coroutine.resume(co,10,10) -- true
print(coroutine.status(co)) -- dead
print(coroutine.resume(co, 20, 10)) -- false cannot resume dead coroutine
这边程序,一开是激活协同程序,并且传入了参数,如果由yield返回给resume
打印resume能够得到值,后续传入参数,并不起效
相应地,resume 的参数,会被传递给 yield。
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
的协同称为不对称协同(asymmetric coroutines)
,指“挂起一个正在执行的协同函数”
与“使一个被挂起的协同再次执行的函数”是不同的,有些语言提供对称协同(symmetric
coroutines),即使用同一个函数负责“执行与挂起间的状态切换”。
有人称不对称的协同为半协同,另一些人使用同样的术语表示真正的协同,严格意
义上的协同不论在什么地方只要它不是在其他的辅助代码内部的时候都可以并且只能使
执行挂起,不论什么时候在其控制栈内都不会有不可决定的调用。(However, other people use the same term semi-coroutine to denote a restricted implementation of coroutines,
where a coroutine can only suspend its execution when it is not inside
any auxiliary function, that is, when it has no pending calls in its
control stack.)。只有半协同程序内部可以使用 yield, python
中的产生器(generator)就是这种类型的半协同。
与对称的协同和不对称协同的区别不同的是,协同与产生器的区别更大。产生器相
对比较简单,他不能完成真正的协同所能完成的一些任务。熟练使用不对称的协同之后,可以利用不对称的协同实现比较优越的对称协同
。
协同程序,最具代表性的例子是用来解决生产者-消费者问题。
假定有一个函数不断的生产数据(比如从文件中读取),另一个函数不断的处理这些数据(比如写到另一文件中)
函数如下:
function producer()
while true do
local x = io.read('*num') -- 生产一个新值
send(x) -- 发送给消费者 consumer
end
end
function consumer()
while true do
local x = receive() -- 接受从生产者
io.write(x, '\n') -- 使用这个新增
end
end
例子中生产者和消费者都在不停的循环,修改一下使得没有数据的时候他们停下
来并不困难),问题在于如何使得 receive 和 send 协同工作。只是一个典型的谁拥有主循环的情况,生产者和消费者都处在活动状态,都有自己的主循环,都认为另一方是可调用的服务。对于这种特殊的情况,可以改变一个函数的结构解除循环,使其作为被动的接受。然而这种改变在某些特定的实际情况下可能并不简单。
协同为解决这种问题提供了理想的方法,因为调用者与被调用者之间的 resume-yield
关系会不断颠倒。当一个协同调用 yield 时并不会进入一个新的函数,取而代之的是返回
一个未决的 resume 的调用。相似的,调用 resume 时也不会开始一个新的函数而是返回
yield 的调用。这种性质正是我们所需要的,与使得 send-receive 协同工作的方式是一致
的。receive 唤醒生产者生产新值,send 把产生的值送给消费者消费。
简单来说【这两个函数都不停的在执行,那么问题来了,怎么来匹配send和recv呢?究竟谁先谁后呢?两者各自有自己的主循环
】
可以通过过滤器的方法,传输的过程当中进行程序的处理
-生产者消费者问题
local newProductor
--生产
function productor()
local i = 0
while true do
i = i + 1
send(i) --将生产的物品发给消费者
end
end
--消费
function consumer()
while true do
local i = recieve() --从生产者哪里拿到物品
print(i)
end
end
--从生产者哪里得到物品的方法
function recieve()
local status,value = coroutine.resume(newProductor)
return value
end
--生产者把物品发送给消费者的方法
function send(x)
coroutine.yield(x) --生产者把值发送出去之后,就把自己的这个协同程序给挂起
end
--启动程序
newProductor = coroutine.create(productor) --创建协同程序newProductor,(创建时执行productor()方法)
consumer()
coroutine.create()
创建 coroutine,返回 coroutine, 参数是一个函数,当和 resume 配合使用的时候就唤醒函数调用
coroutine.resume()
重启 coroutine,和 create 配合使用
coroutine.yield()
挂起 coroutine,将 coroutine 设置为挂起状态,这个和 resume 配合使用能有很多有用的效果
coroutine.status()
查看 coroutine 的状态注:coroutine
的状态有三种:dead,suspended,running,具体什么时候有这样的状态请参考下面的程序
coroutine.wrap()
创建 coroutine,返回一个函数,一旦你调用这个函数,就进入 coroutine,和 create 功能重复
coroutine.running()
返回正在跑的 coroutine,一个 coroutine 就是一个线程,当使用running的时候,就是返回一个 corouting
的线程号
不需要唤醒,创建则执行
co=coroutine.wrap(
function (a,b)
print(a+b)
end
)
co(20,30) -- 50
co=coroutine.create(
function (a,b)
print(a+b)
coroutine.yield()
print(a-b)
end
)
coroutine.resume(co,1,2)
print("I'm here!")
coroutine.resume(co)
3
I'm here!
-1
coroutine 的状态有三种:dead,suspended,running
co=coroutine.create(
function (a,b)
print(a+b)
print(coroutine.status(co)) --running
print(a+b)
coroutine.yield()
print(a-b)
end
)
print(coroutine.status(co)) --此时未启动协程,suspended
coroutine.resume(co,10,20)
print(coroutine.status(co)) --suspended
print("I'm here!")
coroutine.resume(co)
print(coroutine.status(co)) --dead
suspended
30
running
30
suspended
I'm here!
-10
dead
co=coroutine.create(
function (a,b)
print( coroutine.running() ) --thread: 00A78110
coroutine.yield()
print(a-b)
end
)
print( coroutine.running() ) --nil
coroutine.resume(co,10,40)
print("I'm here!")
coroutine.resume(co)
nil
thread: 00BED7E8
I'm here!
-30
协程可返回多个值,第一个值为布尔值,表示协程是否启动成功,后面即为协程函数返回值
状态,值=coroutine.resume(co)
co=coroutine.create(
function (a,b)
coroutine.yield(a*b,a/b)
return a%b,a/b+1
end
)
res1,res2,res3 = coroutine.resume(co,10,40)
print(res1,res2,res3) --true 400 0.25
print("I'm here!")
res1,res2,res3 = coroutine.resume(co)
print(res1,res2,res3) --true 10 1.25
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) -- 这里是将程序的计算结果返回给了resume,所以得不到r的值 因此为nil
print("第二次协同程序执行输出", r)
local r, s = coroutine.yield(a + b, a - b) -- a,b的值为第一次调用协同程序时传入,同样此时r,s也是没有值的
print("第三次协同程序执行输出", r, s)
return b, "结束协同程序" -- b的值为第二次调用协同程序时传入
end)
print("main", coroutine.resume(co, 1, 10)) -- true, 4
print("main", coroutine.resume(co)) -- true 11 -9
print("main", coroutine.resume(co, "x", "y")) -- true 10 end
print("main", coroutine.resume(co, "x", "y")) -- false cannot resume dead coroutine
遍历某个数组的所有排列组合形式
function permgen(a, n)
n = n or #a -- 默认n为a的大小
if n <= 1 then -- 还需要改变吗?
printResult(a)
else
for i=1,n do
-- 将第一个元素放到数组末尾
a[n], a[i] = a[i], a[n]
-- 生成其余元素的排列
permgen(a, n-1)
-- 恢复第i个元素
a[n], a[i] = a[i], a[n]
end
end
end
然后,还需要定义其中调用到的打印函数printResult,并以适当的参数来调用permgen:
function printResult (a)
for i, v in ipairs(a) do
io.write(v, " ")
end
io.write("\n")
end
permgen({ 1, 2, 3, 4 }, 4)
输出如下:
2 3 4 1
3 2 4 1
3 4 2 1
4 3 2 1
2 4 3 1
4 2 3 1
4 3 1 2
3 4 1 2
3 1 4 2
1 3 4 2
4 1 3 2
1 4 3 2
2 4 1 3
4 2 1 3
4 1 2 3
1 4 2 3
2 1 4 3
1 2 4 3
2 3 1 4
3 2 1 4
3 1 2 4
1 3 2 4
2 1 3 4
1 2 3 4
当生成函数完成后,将其转换为一个迭代器就非常容易了。首先,将printResult改为yield:
function permgen (a, n)
if n == 0 then
coroutine.yield(a)
else
for i = 1, n do
-- put i-th element as the last one
a[n], a[i] = a[i], a[n]
-- generate all permutations of the other elements
permgen(a, n - 1)
-- restore i-th element
a[n], a[i] = a[i], a[n]
end
end
end
function printResult (a)
for i, v in ipairs(a) do
io.write(v, " ")
end
io.write("\n")
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语句中遍历一个数组中的所有排列就非常简单了:
for p in permutations {"a", "b", "c"} do
printResult(p)
end
--> b c a
--> c b a
--> c a b
--> b a c
--> a b c
permutations函数使用了一种在Lua中比较常见的模式,就是将一条唤醒协同程序的调用包装在一个函数中。由于这种模式比较常见,所以Lua专门提供了一个函数coroutine.wrap来完成这个功能。类似于create,wrap创建了一个新的协同程序。但不同的是,wrap并不是返回协同程序本身,而是返回一个函数。每当调用这个函数,即可唤醒一次协同程序。但这个函数与resume的不同之处在于,它不会返回错误代码。当遇到错误时,它会引发错误。若使用wrap,可以这么写permutations:
function permutations (a)
return coroutine.wrap(function () permgen(a) end)
end
通常,coroutine.wrap比couroutine.create更易于使用。它提供了一个对于协同程序编程实际所需的功能,即一个可以唤醒协同程序的函数。但也缺乏灵活性。无法检查wrap所创建的协同程序的状态,此外,也无法检测出运行时的错误
如前面所见,Lua 中的协同是一协作的多线程,每一个协同等同于一个线程,yield-resume 可以实现在线程中切换。然而与真正的多线程不同的是,协同是非抢占式的
。
当一个协同正在运行时,不能在外部终止他。只能通过显示的调用 yield 挂起他的执行。对于某些应用来说这个不存在问题,但有些应用对此是不能忍受的
。不存在抢占式调用的程序是容易编写的。不需要考虑同步带来的 bugs,因为程序中的所有线程间的同步都是显示的。你仅仅需要在协同代码超出临界区时调用 yield 即可。
对非抢占式多线程来说,不管什么时候只要有一个线程调用一个阻塞操作(blocking operation),整个程序在阻塞操作完成之前都将停止
。对大部分应用程序而言,只是无法忍受的,这使得很多程序员离协同而去。下面我们将看到这个问题可以被有趣的解决。
看一个多线程的例子:我们想通过 http 协议从远程主机上下在一些文件。我们使用
Diego Nehab 开发的 LuaSocket 库来完成。我们先看下在一个文件的实现,大概步骤是打开一个到远程主机的连接,发送下载文件的请求,开始下载文件,下载完毕后关闭连接。
-- 第一加载luasocket库
require 'socket'
------ 第二定义远程主机和需要下载的文件名
host = 'www.w3.org'
file = "/TR/REC-html32.html"
------ 第三打开一个TCP连接到远程主机的80端口(http服务的标准端口)
conn = assert(socket.connect(host,80))
print(conn) -- tcp{client}: 009E9040
------ 上面这句返回一个连接对象,我们可以使用这个连接
conn:send("GET " .. file .. " HTTP/1.0\r\n\r\n")
------ recevice函数返回他送接收到的数据加上一个表示操作状态的字符串。当主机断开连接时,我们退出循环。
repeat
local chunk,status,partial = conn:receive(1024) -- 以 1K 的字节块来接收数据,并把接收到字节块输出来
print(chunk)
print('-----------------')
print(status)
print('-----------------')
print(partial)
print('-----------------')
print("chuck:size: ",string.len(chunk or partial),status or "ok")
until status == "closed"
---- 第四关闭连接
conn:close()
同时下载多个文件(避免下载单个文件的时候在等待)
require 'socket'
function download(host, file)--并不关心file中的内容
local conn = assert(socket.connect(host, 80))
local count = 0 -- counts number of bytes read
conn:send("GET " .. file .. " HTTP/1.0\r\n\r\n")
while true do
local s, status, partial = receive(conn)
count = count + #(s or partial)
if status == "closed" then
break
end
end
conn:close()
print(file, count)
end
这个函数实际上阻塞的,对应上面函数
function receive(connection)
return connection:receive(2^10)
end
将其改为非阻塞,timeout(0)使得对连接的任何操作都不会阻塞
,注意:即使在 timeout 模式下,连接依然返回他接受到值直到 timeout 为止,因此 receive 会一直返回 s 给她的调用者
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
代码中 table 中为分配器保存了所有活动的线程。
分配器代码是很简单的,它是一个循环,逐个调用每一个线程。并且从线程列表中
移除已经完成任务的线程。当没有线程可以运行时退出循环。
function dispatcher()
while true do
local n = table.getn(threads)
if n == 0 then break end
for i=1,n do
local status,res = coroutine.resume(threads[i])
print('status:',status,'---','res:',res)
if not res then -- 线程是否结束其任务
table.remove(threads,i)
break
end
end
end
end
if not res then
判断这一个线程任务处于挂起状态的话,返回的是nil,说明正则进行,则从table中移除此挂起线程,执行线程池剩下的线程
最后,在主程序中创建需要的线程调用分配器,例如:从 W3C 站点上下载 4 个文
件
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/DOM2-Core.txt")
dispatcher() -- main loop
local socket = require "socket"
local host = "www.baidu.com"
local file = "/"
local HTTP = "HTTP/1.0\r\n\r\n"
local sock = assert(socket.connect(host,80)) -- 创建一个 TCP 连接,连接到 HTTP 连接的标准 80 端口上
sock:send("GET " .. file .. HTTP)
repeat
local chunk,status,partial = sock:receive(1024) -- 以 1K 的字节块来接收数据,并把接收到字节块输出来
print("chuck:size: ",string.len(chunk or partial),status or "ok")
until status == "closed"
sock:close()
http=require("socket.http")
result,status,headers=http.request("http://www.hz.gov.cn") -- 结果,状态码,返回内容
print(result)
print(status)
print(headers)
for i,v in pairs(headers) do
print(i,v)
end