九、编译、执行与错误
Lua尽管是一种解释型语言,但也允许它在运行源代码之前预编译成一种中间形式。但解释型语言的特征在于,编译器是语言运行时的一部分,并且有能力执行动态生产的代码。因此执行编译产生的中间码速度更快。
1、编译
前面介绍到dofile函数用于执行代码块。但dofile实际上是调用loadfile实现的。dofile加载并执行文件,而loadfile只加载文件进行编译,而不执行。
loadfile(filename)编译文件并返回一个函数。如果发生错误则返回nil和错误信息。
f=loadfile(filename) --加载编译
f() --执行
由于可能返回nil,因此执行执行f存在风险。可以使用assert函数:
f=assert(loadfile(filename))
如果loadfile失败,assert抛出错误,否则返回函数。
loadfile在加载并编译文件后得到一个函数,可以多次调用这个函数来执行,而dofile则每次都要加载执行,开销要大得多。
相似的,有loadstring(string)来从字符串中读取代码并编译:
f=loadstring(“lcoali=0;print(i)”)
f()
相当于定义一个函数:
f=function () lcoali=0;print(i) end
同样,用assert来检验加载情况,并且可直接执行:
assert(loadstring(s))()
但是函数的定义比用loadstring的方式快得多。因此函数制造编译时对其编译一次,而每次调用loadstring都要重新编译。
还有一个区别是loadstring不涉及词法域(变量的作用范围):
i=22
local i = 0
f =loadstring("i = i + 1;print(i)")
g = function () i =i + 1; print(i) end
f() -->23
g() -->1
原因是loadstring是对全局环境编译它的字符串的,而函数g操作了局部i。
loadstring最典型的应用是执行外部代码,也就是位于程序之外的代码。例如可以让用户输入代码,在程序内部调用loadstring去执行。如果需要函数值,可以在字符串中加return。
loadfile和loadstring是基于原始函数load的。load接收一个“读取器函数“,并在内部调用它来获取程序块。读取器函数可以分几次返回一个程序块,直到返回nil(表示程序块结束)。一般很少使用load,只有当程序块不在文件中(动态创建或需从特定源头读取的代码),或者代码块过大而无法放入内存时,才用到它。
用loadfile或loadstring后并没成对其中的函数的定义——函数定义是一种赋值操作,是运行时完成的。如有foo.lua文件:
function foo (x)
print(x)
end
f= loadfile("foo.lua")后,foo 被编译了但还没有被定义,如果要定义他必须运行chunk:
f() -- defines `foo'
foo("ok") --> ok
Lua将独立的程序块视为一个匿名函数的函数体,并且该匿名函数具有可变长的参数。loadstring(“a=1”)等价于
function (…) a=1end
2、C代码
Lua采用动态连接机制来加载C程序库。在Lua提示符中运行print(package.loadlib(”a”,”b”))来检查当前系统是否支持Lua的动态连接机制。若输出nil和找不到模块则表示支持。
Lua提供的所有关于动态的功能都聚集在package.loadlib函数中。这个函数有两个参数:库的绝对路径和初始化函数。例如:
local path ="/usr/local/lua/lib/libluasocket.so"
--local f =loadlib(path, "luaopen_socket")
local f = assert(loadlib(path,"luaopen_socket"))
f() -- actually open the library
loadlib函数加载指定的库并连接到Lua。但它并不打开库,即不调用库中的任何函数,而是将这个初始化函数(C函数)作为一个Lua函数返回(即上面的f),这样就可以在Lua中调用了。如果加载动态库或者查找初始化函数时出错,loadlib 将返回nil 和错误信息。
3、错误
Lua通常嵌入到应用程序中,因此在发生错误时不能简单地退出,而是应该结束当前程序块并返回应用程序。这就需要检验错误和抛出错误。
当试图对两个非数字相加时会报错,同样也可以显式的抛出一个错误:
error(“invalidinput”)
-->stdin:1: invalid input
通常用“if not<condition> then error(s) end”的方式来抛出异常。error的参数是一个错误消息,通常是一个字符串,但也可以其他任何类型的值。
Lua 提供了专门的内置函数 assert 来完成上面类似的功能:
n =assert(io.read("*number"), "invalid input")
assert首先检测第一个参数是否为true,若true则单纯地返回两个参数;否则以第二个参数抛出错误信息。若上面的io.read("*number")为假,则会抛出stdin:1: invalid input的错误信息。
当函数遇到异常有两个基本的动作:返回错误代码(通常是nil)或者抛出错误(调用error)。这两种方式选择哪一种没有固定的规则,但有一般的原则:容易避免的异常应该抛出错误否则返回错误代码。
例如当math.sin计算一个非数字值时只给出一个错误消息并停止计算;而对于io.open的文件操作则会返回错误代码(打开失败返回nil),这样可以采取适当的异常处理。
4、错误处理与异常
在解释器程序中发生错误,会打印错误。若要在脚本中处理错误就要用到pcall(protected call,保护模式的调用)。
用pcall来捕获一段代码的异常需要以下步骤:
S1.将这段代码封装在一个函数中
function foo ()
…
if 未预期的条件 then error() end
…
print(a[i]) --潜在的错误:a可能不是一个table
…
end
S2.用pcall调用这个函数
if pcall(foo) then
<foo没有发生错误的代码>
else
< foo发生错误时的处理代码>
end
pcall函数在foo没有发生错误时返回true及foo的返回值;否则返回false及错误消息。
可以在error中传入任何类型的数据,并且这些值可以成为pcall的返回值:
localstatus,err=pcall(function () error({code=121}) end)
print(err.code) -->121
此处error的参数是一个table。
Lua通过error抛出异常,pcall捕捉的方式来处理异常。
5、错误消息与追溯(traceback)
虽然可以用任何类型的值作为错误消息,但错误消息一般是描述出错内容的字符串。如果遇到内部错误(比如对一个非 table的值使用索引下表访问)Lua将自己产生错误信息,否则 Lua 使用传递给 error函数的参数作为错误信息。
local status, err =pcall(function () a = 'a'+1 end)
print(err)
--> stdin:1:attempt to perform arithmetic on a string value
local status, err =pcall(function () error("my error") end)
print(err)
--> stdin:1: myerror
第一段代码是Lua自己的错误信息;第二段是error抛出的错误信息。位置信息中包含了文件名(stdin)和行号(1)。
error函数可以添加第二个参数来指定错误的层级。比较下面两段程序:
function foo (str)
if type(str) ~= "string" then
error("string expected") --error错误层级,定位到该行
end
end
foo(1)
------------------------------------
function foo (str)
if type(str) ~= "string" then
error("stringexpected", 2) --error错误层级,定位到foo(1)
end
end
foo(1)
调用foo(1)的时候实际上错误发生在调用者身上(没有传入字符串),而不是foo函数本身。但第一段代码没有指定error的层级,错误会定位在error语句。而第二段代码指定了错误发生在调用层级的第二层(自己函数定义是第一层),错误定位在foo(1)。
当错误发生的时候,我们常常需要更多的错误发生相关的信息,而不单单是错误发的位置。至少期望有一个完整的显示导致错误发生的调用栈的 tracebacks,当 pcall 返错误信息的时候他已经释放了保存错误发生情况的栈的信息。因此,如果我们想得到tracebacks我们必须在 pcall 返回以前获取。Lua 提供了 xpcall 来实现这个功能,xpcall接受两个参数:调用函数和错误处理函数。当错误发生时。Lua 会在栈释放以前调用错误处理函数,因此可以使用 debug 库收集错误相关的信息。有两个常用的 debug 处理函数:debug.debug 和debug.traceback,前者给出 Lua的提示符,你可以自己动手察看错误发生时的情况;后者通过 traceback 创建更多的错误信息,后者是控制台解释器用来构建错误信息的函数。你可以在任何时候调用debug.traceback获取当前运行的traceback信息:
print(debug.traceback())
十、协同程序(coroutine)
协同程序与多线程相似:都有自己独立的栈、局部变量和指令指针,以及和其他协同程序共享全局变量等信息。线程与协同程序的区别在于:在多处理器情况下,多线程程序可以同时运行多个线程;而协同程序是通过协作来完成的。也就是说,任意时刻只有一个协同程序在运行,并且这个运行中的协同程序只会在显式地要求挂起时才会暂停执行。
1、协同程序基础
关于协同程序的创建、运行、挂起等工作都定义在名为“coroutine”的table中:
函数 | 说明 | 例子 |
coroutine.create(f) | 创建一个新的协同程序,返回一个thread类型数据。参数是一个函数,通常是一个匿名函数,该函数表示协同程序要执行的内容。 |
co=coroutine.create( function () print("ok") end) |
coroutine.resume(thread) | 启动或再次启动一个协同程序,将其状态由挂起改为运行。第一个返回值为true表示成功。 |
coroutine.resume(co) |
coroutine.status(thread) | 查看一个协同程序的状态(挂起、运行、死亡、正常)。 |
coroutine.status(co) |
coroutine.yield() | 挂起一个运行中的协同程序。只有协同程序的主函数才可以调用,表示挂起当前coroutine。 |
coroutine.yield() |
①创建一个协同程序后,它处于挂起状态;挂起是指进程被操作系统移除内存,在条件允许的情况下才再次回到内存,处于就绪状态。
②resume是在保护模式下运行的。所以协同程序中发生错误都不会显示错误消息,而是将其返回给resume。
③当一个协同程序A唤醒两一个协同程序B时,A处于一个特殊状态:既不是挂起也不是运行,而是处于“正常”状态。
★ yield与resume
yield用于挂起协同程序,而resume恢复执行。两者组合使用可以用于交换数据。
程序①:
local co =coroutine.create(function(name)
print(name);
end);
coroutine.resume(co, "resumeparam"); -->resume param
该程序没有用到yield,resume传入的参数将作为协同程序主函数的参数,因此输出resume传入的参数。
程序②:
local co5=coroutine.create(function(name)
fori=1,2 do
print(name)
print("co5",coroutine.yield("yieldparam"))
end
return"co5 end"
end)
for i=1,3 do
print("-------第" .. i .. "次执行")
localresult,msg=coroutine.resume(co5,"resume param")
print("msg:"..msg)
end
上述程序的执行流程:
1) print("-------第" .. i .. "次执行"),输出“-------第1次执行”;
2)resume启动co5,并且传入参数"resumeparam";
3)执行co5主函数:print(name),输出"resume param";
4)执行co5主函数:print("co5",coroutine.yield("yieldparam")),但co5被挂起
5)resume返回:yield的参数作为resume的第二个参数返回
6)print("msg:".. msg):输出msg:yield param
7) print("-------第" .. i .. "次执行"),输出“-------第2次执行”;
8)resume启动co5,由于之前co5被挂起,此时被恢复,因此得到上一次print("co5",coroutine.yield("yieldparam"))的结果,输出“co5 resume param”——yield的返回值是resume的参数;
9)—12)同3)—6)
13)print("-------第" .. i .. "次执行"),输出“-------第3次执行”;
14)由于之前co5被挂起,此次resume只是恢复co5,所以得到第二次挂起的结果,输出“co5 resume param”
15)主函数中循环2次,所以会被挂起2次,需要3个resume才能是主函数执行完,最后return"co5 end",该返回值返回到resume,作为其返回值;
16)输出"co5 end";至此程序结束。
全部输出如下:
-------第1次执行
resume param
msg:yield param
-------第2次执行
co5 resume param
resume param
msg:yield param
-------第3次执行
co5 resume param
msg:co5 end
所谓的“交换数据”是指resume除true以外的返回值是yield的参数;yield的返回值是调用resume时除thread外传入的参数!
在n次循环中yield协同程序,需要n+1次调用resume才能使主程序全部结束并return其返回值。而第n+1次的调用只是把第n次挂起的协同程序恢复,而不会执行yield语句之前的语句了!主程序的返回值也会传给resume作为resume的返回值。
当一个协同程序dead后,若再企图resume,则会返回false,并给出“cannot resumedead coroutine”的错误信息(需要printresume的返回值)。
Lua提供的这种协同我们称为“不对称的协同程序”,就是说挂起一个正在执行的协同的函数与使一个被挂起的协同再次执行的函数是不同的,有些语言提供对称的协同,这种情况下,由执行到挂起之间状态转换的函数是相同的。
在“不对称的协同程序”中,一个协同程序只有在它没有调用其他函数时(它的调用栈中没有未完成的调用),才能挂起执行。也就是说只有协同程序的主函数才能调用yield函数。
2、管道与过滤器
协同程序最经典的例子就是“生产者-消费者”模式,比如一个读取文件,一个写文件。关键在于如何共享数据的问题。在Java中通常用一个公共的队列在存储生产和消费的内容。Lua中可以通过resume-yield对来实现数据的互通。
例如有一个receice函数用于接收“产品”,生产者用send函数发出“产品”,consumer函数消费“产品”:
function receive()
localstatus,value=coroutine.resume(producer)
return value
end
function send(x)
coroutine.yield(x)
end
consumer=function()
for i=1,5 do --文件只有5行
local x=receive()
print(x)
end
end
因此,生产者必须是一个协同程序:
producer=coroutine.create(function()
local x=true
while x do
local x=io.read()
send(x)
end
end)
上述程序receive函数唤醒生产者的执行,产生一个新值。其中value是resume的第二个返回值等于yield的参数;而send负责发送新值给消费者(通过resume-yield),并挂起协同程序。当消费者需要一个新值时,它唤醒生产者。生产者返回一个新值后挂起,并等待消费之唤醒。因此启动上述程序只要执行consumer函数即可。
上述程序可以经扩展,加入“过滤器”。过滤器是生产者和消费者之间的角色,用于处理生产者的“产品”,实现数据变换。因此过滤器既是一个消费者(消费原生产者的“产品”并经过加工)也是一个生产者(传递给原消费者)。
functionreceive2(prod)
local status,value=coroutine.resume(prod)
return value
end
function send2(x)
coroutine.yield(x)
end
functionproducer2()
return coroutine.create(function ()
while true do
local x=io.read()
send2(x)
end
end)
end
--过滤器:先接收,经过处理再发出。既是消费者也是生产者
functionfilter(prod)
return coroutine.create(function()
for line=1,math.huge do
local x=receive2(prod)
x=string.format("%5d%s",line,x)--加入原数据前行号
send2(x)
end
end)
end
functionconsumer2(prod)
for i=1,5 do
local x=receive2(prod)
print("filter_consumer:",x)
end
end
--执行
p=producer2()
f=filter(p)
consumer2(f)
协同是一种非抢占式的多线程。管道的方式下,每一个任务在独立的进程中运行,而协同方式下,每个任务运行在独立的协同代码中。管道在读(consumer)与写(producer)之间提供了一个缓冲,因此两者相关的的速度没有什么限制,在上下文管道中这是非常重要的,因为在进程间的切换代价是很高的。协同模式下,任务间的切换代价较小,与函数调用相当,因此读写可以很好的协同处理。
3、协同程序实现迭代器
将循环迭代看做“生产者-消费者”模式的一个特例(迭代器生产,循环消费),就可以用resume-yield来实现迭代器了。下面是一个生成数组元素所有排列组合的迭代器。
--生成一个数组所有排列组合:将每个元素放到最后一个位置,然后递归地生成其余元素的排列
functionpermgen2(a,n)--生成函数
n = n or #a
if n <= 1 then
coroutine.yield(a)
else
for i=1,n do
a[n] ,a[i] = a[i] ,a[n] --交换a[i] ,a[n]
permgen2(a,n-1) --生成前面的序列
a[n] ,a[i] =a[i],a[n] --恢复原序列
end
end
end
functionpermutations(a)--工厂函数
--将生成函数放到协同程序中运行
local co=coroutine.create(function ()
permgen2(a)
end)
--创建迭代器函数
return function()
localcode,res=coroutine.resume(co)
return res
end
end
for p inpermutations({"a","b","c"}) do
printResult(p)
end
permutations是一个工厂函数,用于将生成函数(permgen)放到一个协同程序中运行,并创建迭代器函数。迭代器函数简单地唤醒协同程序,让其产生下一种排序。
上面的这种模式Lua中常见的模式,即将一条唤醒协同程序的调用包装在一个函数中。Lua专门提供了coroutine.wrap函数来实现这一模式。wrap与create相同之处是创建一个新的协同程序。不同之处是wrap并不返回协同程序本身,而是返回一个函数,当这个函数被调用时将 resume 协同程序。上述例子改为wrap:
functionpermutations2(a)
return coroutine.wrap(function ()permgen2(a) end)
end
for p inpermutations2({"x","y","z"}) do
printResult(p)
end
可以看做wrap返回了一个迭代器函数,其包含了resume语句。在泛型for中会调用wrap返回的函数,来resume协同程序。
但wrap 中 resume 协同的时候不会返回错误代码作为第一个返回结果,一旦有错误发生,将抛出错误。
一般情况下,coroutine.wrap比 coroutine.create 使用起来简单直观,前者更确切的提供了我们所需要的:一个可以 resume 协同的函数,然而缺少灵活性,没有办法知道 wrap所创建的协同的状态,也没有办法检查错误的发生。
4、非抢占式多线程
协同程序与常规多线程不同之处在于,协同程序是非抢占式的,即一个协同程序运行时,是无法从外部停止它的。只有当协同程序显式的要求挂起时才会停止。非抢占式对同步编程变得简单:程序中所有线程间的同步都是显式的,只需确保一个协同程序在它的临界区之外调用yield即可。
由于其非抢占式的特点,除非主动放弃CPU,否则其他协同程序无法占有CPU,所以当占用CPU的协同程序阻塞时,整个程序在阻塞操作完成之前都将停止。也就是说不玩完成多线程的轮流占用CPU工作。而在抢占式系统中,由操作系统支配各个进程的工作与否,从而使得各个进程可以交替进行。
为解决上面的问题,提供一种“调度器”的解决方法,实现Lua的抢占式工作。即一个调度程序来掌握控制权,选择要启动的协同程序。
--多线程下载多个文件的程序
require"socket"--导入socket库
--文件下载函数
functiondownload(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" thenbreak end
end
c:close()
print(file,count)--只计算文件大小
end
--数据接收函数
functionreceive(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={} --记录所有线程的table
functionget(host,file)--创建新的下载线程
local co=coroutine.create(function ()
download(host,file)
end)
table.insert(threads,co)
end
--调度函数
function dispatch()
while true do
local n=table.getn(threads)
if n==0 then break end --表为空,表示全部结束
for i=1,n do -- 唤醒执行线程
localstatus,res=coroutine.resume(threads[i])
if not res then
table.remove(threads,i)--完成即移除线程
break
end
end
end
end