1.迭代器与泛型for
1.1 迭代器与闭包
迭代器是一种支持指针类型的结构,它可以遍历集合的每一个元素。在Lua中我们常常使用函数来描述迭代器,每次调用该函数就返回集合的下一个元素。
迭代器需要保留上一次成功调用的状态何下一次成功调用的状态。闭包提供的机制可以很容易实现这个任务。
一个典型的闭包的结构包含两个函数:一个是闭包自己;另一个是工厂(创建闭包的函数)。
举一个简单的例子,我们为一个list写一个简单的迭代器,与ipairs()不同的是我们实现的这个迭代器返回元素的值而不是索引下标:
function list_iter (t)
local i = 0
local n = table.getn(t)
return function()
i = i + 1
if i <= n then return t[i] end
end
end
这个例子中 list_iter 是一个工厂,每次调用他都会创建一个新的闭包(迭代器本身)。闭包保存内部局部变量(t,i,n),因此每次调用他返回 list 中的下一个元素值,当 list 中没有值时,返回 nil.
范性 for 为迭代循环处理所有的薄记(bookkeeping):首先调用迭代工厂;内部保留迭代函数 ;然后在每一个新的迭代处调用迭代器函数;当迭代器返回 nil 时循环结束。
1.2范性for的语义
前面我们看到的迭代器有一个缺点:每次调用都需要创建一个闭包,然而在有些情况下创建闭包的代价是不能忍受的。在这些情况下我们可以使用范性for本身来保存迭代的状态。
我们看到在循环过程中范性for在自己内部保存迭代函数,实际上它保存三个值:迭代函数,状态常量和控制变量。下面详细说明。
范性for的文法如下:
for <var-list> in <exp-list> do
<body>
end
< var-list >是一个或多个以逗号分割的变量名列表,< exp-list >是一个或多个逗号分割的表达式列表,通常情况下exp-list只有一个值:迭代工厂的调用。
for k,v in pairs(t) in
print(k,v)
end
我们称变量列表中第一个变量为控制变量,其值为 nil 时循环结束。
下面我们看看范性for的执行过程:
- 初始化,计算in后面表达式的值,表达式应该返回范性for需要的三个值:迭代函数,状态常量和控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用nil补足,多出部分会被忽略。
- 将状态常量和控制变量作用参数调用迭代函数(注意:对于 for 结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)
- 将迭代函数返回的值赋给变量列表。
- 如果返回的第一个值为nil循环结束,否则执行循环体。
- 回到第二步再次调用迭代函数。
1.3无状态的迭代器
无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。
每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个只利用这两个值可以获取下一个元素。这种无状态迭代器的典型的简单的例子是ipairs,他遍历数组的每一个元素。
a = {"one", "two", "three"}
for i,v in ipairs(a) do
print(i,v)
end
迭代的状态包括被遍历的表(循环过程中不会改变的状态常量)和当前的索引下标(控制变量),ipairs和迭代函数都很简单,我们在Lua中可以这样实现:
function iter(a,i)
i = i+1
local v=a[i]
if v then
return i,v
end
end
function ipairs(a)
return iter,a,0
end
当Lua调用ipairs(a)开始循环时,他获取三个值:迭代函数iter,状态常量a和控制变量初始值0;然后Lua调用iter(a0,)返回1,a[1];第二次迭代调用iter(a,1)返回2,a[2]......直到第一个非nil元素。
1.4多状态的迭代器
很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量何控制变量.最简单的办法是使用闭包,还有一个方法就是将所有的状态信息封装到table内,将table作为迭代器的状态常量。
下面我们重写allwords迭代器,这一次我们不是使用闭包而是使用带有两个域(line,pos)的table。
开始迭代的函数是很简单的,他必须返回迭代器函数何初始状态:
local iterator --to be defined later
function allwords()
local state={line=io.read(),pos=1}
return iterator,state
end
真正的处理工作是再迭代函数内完成:
function iterator(state)
while state.line do --repeat while there are lines
--search for next word
local s,e=string.find(state.line,"%w+",state.pos)
if s then --found a word?
state.pos=e+1
return string.sub(state.line,s,e)
else --word not found
state.line=io.read() --try next line...
state.pos=1 --... from first position
end
end
return nil --no more lines:end loop
end
我们应该尽可能的写无状态的迭代器,因为这样循环的时候由 for 来保存状态,不需要创建对象花费的代价小;如果不能用无状态的迭代器实现,应尽可能使用闭包:尽可能不要使用table这种方式,因为创建闭包的代价要比创建table小,另外Lua处理闭包要比处理table速度快些。
1.5真正的迭代器
迭代器的名字有一些误导,因为它并没有迭代,完成迭代功能的是for语句,也是更好的叫法应该是'生成器';但是再其他语言比如java、C++迭代器的说法已经很普遍了,我们也将沿用这种术语。
有一种方式创建一个在内部完成迭代的迭代器。这样当我们使用迭代器的时候就不需要使用循环了;我们仅仅使用每一次迭代需要处理的任务作为参数调用迭代器即可,具体地说,迭代器接受一个函数作为参数,并且这个函数在迭代器内部被调用。
作为一个具体的例子,我们使用上述方式重写allwords迭代器:
function allwords(f)
--repeat for each line in the file
for l in io.lines() do
--repeat for each word in the line
for w in string.gfin(1,"%w+") do
--call the function
f(w)
end
end
end
如果我们想要打印出单词,只需要
allwords(print)
更一般的做法是我们使用匿名函数作为参数。
2.协同程序
协同程序(corountine)与多线程情况下的线程比较类似:有自己的堆栈,自己的局部变量,有自己的指令指针,但是和其他协同程序共享全局变量等很多信息。
线程和协同程序的主要不同在于:在多处理器情况下,从概念上来讲多线程程序同时运行多个线程;而协调程序是通过协作来完成,在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。
2.1协同的基础
Lua通过table提供所有的协同函数,create函数创建一个新的协同程序,create只有一个参数:协同程序将要运行的代码封装而成的函数,返回值为thread类型的值表示创建了一个新的协同程序。通常情况下,create的参数是一个匿名函数:
co = corountine.create(function ()
print("hi")
end)
print(co) -->thread:0x0871d98
协同有三个状态:挂起态、运行态、停止态。当我们创建一个协同程序时他开始的状态位挂起态,也就是说我们创建协同程序的时候不会自动运行,可以使用status函数检查协同的状态:
print(coroutine.status(co)) -->suspended
函数coroutine,resume可以使程序由挂起来状态变成运行态:
coroutine.resume(co) -->hi
这个例子中,协同体仅仅打印出"hi"之后便进入终止状态:
print(coroutine.status(co)) -->dead
当目前为止,协同看起来只有一种复杂的调用函数的方式,真正的强大指出体现在yield函数,它可以将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
第二个例子,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
2.2通道和过滤器
协同最有代表性的作用是用来描述生产者-消费者问题。
我们假定由一个函数在不断的生产值(比如从文件中读取)另一个函数不断的消费这些值(比如写到到另一个文件中),这两个函数如下:
function producer()
while true do
local x=id.read() --produce new value
send(x) --send to consumer
end
end
function consumer()
while true do
local x=receive() --receive from producer
io.write(x,"
") --consume new value
end
end
(例子中生产者和消费者都在不停的循环,修改下使得没有数据的时候他们停下来并不困难),问题在于
如何使得receive和send协同工作。
当一个协同调用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)
这种设计下,开始时调用消费者,当消费者需要值时他唤起生产者生产值,生产者生产值后停止直到消费者再次请求。我们称这种设计位消费者驱动的设计。
我们可以使用过滤器扩展这个设计,过滤器指在生产者与消费者之间,可以对数据进行某些转换处理。过滤器在同一时间既是生产者又是消费者,他请求生产者生产值并且转换格式后传给消费者,我们修改上面的代码加入过滤器(每一行前面加上行号):
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)
return coroutine.create(function()
local line=1
while true do
local x=receive(prod) --get new value
x=string.format("%5d %s",line,x)
send(x) --send it to consumer
line=line+1
end
end)
end
function consumer(prod)
while true do
local x=receive(prod) --get new value
io.write(x,"
") --consume new value
end
end
可以调用:
p=producer()
f=filter(p)
consumer(f)
--或者
consumer(filter(producer()))
管道的方式下,每一个任务在独立的进程中运行,而协同方式下,每个任务运行在独立的协同代码中。管道在度与写之间提供了一个缓冲。
2.3用作迭代器的协同
协同的一个关键特征是它可以不断颠倒调用者与被调用者之间的关系,这样我们毫无顾虑的使用它实现一个迭代器,而不用保存迭代函数返回的状态。
我们来完成一个打印一个数组元素的所有的排列来阐明这种应用。直接写这样一个迭代函数来完成这个任务并不容易,但是写一个生成所有排列的递归函数并不难。
思路是这样的:将数组中的每一个元素放到最后,一次递归生成所以有剩余元素的排列。代码如下:
function permgen(a,n)
if n == 0 then
printResult(a)
else
for i=1,n do
--put i-th element as lase 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("
")
end
permgen ({1,2,3,4}, 4)
有了上面的生成器后,下面我们将这个例子修改一下使其转换成一个迭代函数:
-
第一步printResult改为yield
function permgen (a, n) if n == 0 then coroutine.yield(a) else ...
-
第二步,我们定义一个迭代工厂,修改生成器在生成器内创建迭代函数,并使生成器运行在一个协同程序内。迭代函数负责请求协同产生下一个可能的排列。
function perm(a) local n=table.getn(a) local co = coroutine.create(function() permgen(a,n) end) return function() --iterator local code,res=coroutine.resume(co) return res end end
这样我们就可以使用 for 循环来打印出一个数组的所有排列情况了
for p in perm{"a", "b", "c"} do printResult(p) end --> b c a --> c b a --> c a b --> a c b --> b a c --> a b c
perm函数使用了Lua中常见的模式:将一个对协同的resume的调用封装在一个函数内部,这种方式在Lua非常常见,所有Lua专门为此提供了一个函数coroutinel.wrap。与create相同的是,wrap创建一个协同程序;不同的是wrap不返回协同本身,而是返回一个函数,当这个函数被调用时将resume协同。wrap中resume协同的时候不会返回错误代码组为第一个返回结果,一旦有错误发生,将抛出错误。我们可以使用wrap重写perm:
function perm(a)
local n=table.getn(a)
return coroutine.wrap(function () permgeen(a,n) end)
end
一般情况下,coroutine.wrap比coroutine.create使用起来简单直观,前者更确切的提供了我们所需要:一个可以resume协同的函数,然后缺少灵活性,没有办法指导wrap所创建的协同的状态,也没有办法检查错误的发生。
非抢占式多线程
与真正的多线程不同的是,协同是非抢占式的,但一个协同正在运行时,不能再外部终止他。只能通过显示的调用yield挂起它的执行。
对于非抢占式多线程来说,不管什么时候只要有一个线程调用一个阻塞操作(bloaking operation),整个程序在阻塞操作完成之前都将停止。
看一个多线程的例子:我们想通过 http 协议从远程主机上下在一些文件。我们使用
Diego Nehab 开发的 LuaSocket 库来完成。我们先看下在一个文件的实现,大概步骤是打
开一个到远程主机的连接,发送下载文件的请求,开始下载文件,下载完毕后关闭连接。
-
第一,加载LuaSocket库
require "luasocket"
-
第二,定义远程主机和需要下载的文件名
host="www.w3.org" file="/TR/REC-html32.html"
-
第三,打开一个TCP连接到远程主机的80端口(http服务的标注端口)
c=assert(socket.connect(host,80))
上面这句返回一个连接对象,我们可以使用这个连接对象请求发送文件
c:send("GET"..file.."HTTP/1.0 ")
receive函数返回他送接收到的数据加上一个表示操作状态的字符串。但主机断开连接时,我们退出循环。
-
第四,关闭连接
c:close()
很显然,当下载多文件时使用协同可以为同时下载提供很方便的支持,我们为每一个下载任务创建一个线程,当一个线程没有数据到达时,他将控制全交给一个分配器,有分配器唤起另外的线程读取数据。
使用协同机制重写上面的代码,在一个函数内:
function download(host,file)
local c=assert(socket.connect(host,80))
local count = 0 --counts number of bytes read
c:send("GET"..file.."HTTP/1.0
")
while true do
local s,status=receive
count=count+string.len(s)
if status == "closed" then break end
end
c:close()
print(file,count)
end
上面只是计算文件大小,在新的函数代码中,我们使用receive从远程连接接收数据,在顺序接收数据的方式下代码如下:
function receive(connection)
return connection:receive(2^10)
end
在同步接收数据的方式下,函数接收数据时不能被阻塞,而是在没有数据可取时yield,代码如下:
function receive(connection)
connection:timeout(0) -- do not block
local s,status=connection:receive(2^10)
if status == "timeout" then
coroutine.yield(connection)
end
return s,status
end