-
什么是lua:
lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放(地址:github.com/zhyingkun/l… )。其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
-
lua特性:
-
轻量级:
使用标准C编写,编译后仅仅一百余K,方便嵌入别的程序中
- 我们都知道Redis中通过运行lua脚本可以执行原子性任务,但为什么不是通过执行某个Java的方法来执行原子任务呢?因为Java太重了,redis的运行不可能把一部分内存再让给JVM;
- 其他许多场景也正是由于lua对资源消耗少,例如游戏开发,独立应用脚本,Web应用脚本,扩展和数据库插件(如 MySQL Proxy,MySQL workbench)
-
可扩展:
lua提供了非常易于使用的扩展接口机制,由宿主语言(C或C++)提供
- 我们Java程序员经常会有需要调用JNI本地代码的场景,直接调用C的代码可能有所不便,而通过lua间接调用C即可充分利用lua的可扩展特性
-
支持面向过程 & 函数式编程
-
自动内存管理,lua只提供一种通用类型的表(table),用他可以实现数组、哈希表、集合、对象。 使用table作为主要数据结构对内存管理很有好处:GC逻辑更简单,内存碎片更少导致GC时延更短...
-
实现 闭包
闭包:是一个由函数 和 该函数能够访问到的 非局部变量 组成的实体。
举个例子:
// 在没有闭包的调用栈中执行 function parent() { let n = 1 // 局部变量n,基本数据类型1 function son() { // 局部变量son // son执行时,parent已经出栈,变量n访问不到了 // 报错! console.log(n); } return son // parent出栈 } let son = parent() // parent入栈 son()// son入栈,问题的矛盾点是:
函数式编程中函数是一等公民,但是函数和常见的基本数据类型或者引用数据类型还是有所不同的,这个不同点就导致无法直接将函数当作其他数据类型来看待。 代码中,函数son作为parent的返回值,被接着执行了,此时son 和 局部变量 n 的关系就很暧昧——理论上访问不到,但是希望它访问的到 —— 需要新技术 —— 闭包;
闭包的具体实现:
将闭包所需要的数据,都存储到堆上(Heap)。
-
通过闭包和table可以方便地支持面向对象的一些关键机制,比如继承与重载,数据抽象,虚函数等。
-
Lua语法
lua被设计成一款使用方便的解释型语言。此特征就导致其运行模式和语法都和python类似
-
交互式编程 & 脚本式编程
聊到这里就需要说明lua的运行模式;lua会首先将代码编译成一种中间状态(字节码),然后由解释器逐行执行。上层的调用只需要执行
lua example.lua即可,底层实则优先由 luac 编译成 example.luac(或者在win下是example.out)文件,之后再解释执行;但是由于lua在设计上不鼓励直接运行.luac文件,所以解释执行的方案需要曲线救国:- 编写启动脚本loader.lua
package.loaded[...] = nil -- 清除可能存在的同名模块 loadfile(arg[1])() -- 加载并执行第一个命令行参数指定的文件 - 命令行中 通过启动脚本运行luac文件
lua loader.lua example.luac
为什么不鼓励?无法解释.luac那为什么要面向开发者提供luac编译器? 因为lua VM的实现方式没有考虑向后兼容,也没有考虑跨平台;也就是说编译得到的.luac文件如何理解,每个版本可能都有所不同。(小众语言,果然不如Java)
- 编写启动脚本loader.lua
-
变量
lua有三种类型的变量:全局变量、局部变量、表中的域。
在默认情况下,变量总是全局的。除非用local显示声明为局部变量(局部变量的作用域为声明位置开始到所在语句块的结束位置)
lua全局变量的实现
在之前写Java程序时,全局变量的语义是用来描述整个jar程序中可见且唯一的一些变量,使用时往往由public static修饰,在类加载器加载.class文件时就将这个变量加载入堆内存中,时刻对外暴露;lua中实现也类似 —— 定义的全局变量会进入_G全局变量表(类似hashmap)中,通过_G[varname]即可访问
不同的点在于:lua的全局变量并非真正的全局变量,其设计遵循环境与模块的隔离
我们先来梳理下声明和使用全局变量的流程:
-
声明很简单,a = 10即可
-
使用:
一个很简单的想法是:全局变量这么多,很容易冲突啊?lua通过环境进行了隔离
对于任意自由名称(没有关联到显式声明上的名称),例如下面的x,y
local z = 10 x = y + z编译器将代码中所有自由名称转化成_ENV.x,因此代码变成
local z = 10 _ENV.x = _ENV.y + z那么_ENV又是什么?
它可以是任意的表,这样的表就被称为一个环境。 通过加载不同的表,声明程序运行的不同环境,从而做到全局变量一定程度上的隔离。
-
-
table
table在lua中用来实现模块、包和对象,本身的抽象程度很高,同时也用来当作数组、字典这种简单的数据结构
基础操作
-- 简单的 table mytable = {} print("mytable 的类型是 ",type(mytable)) mytable[1]= "Lua" mytable["wow"] = "修改前" print("mytable 索引为 1 的元素是 ", mytable[1]) print("mytable 索引为 wow 的元素是 ", mytable["wow"]) -- alternatetable和mytable的是指同一个 table alternatetable = mytable print("alternatetable 索引为 1 的元素是 ", alternatetable[1]) print("alternatetable 索引为 wow 的元素是 ", alternatetable["wow"]) alternatetable["wow"] = "修改后" print("mytable 索引为 wow 的元素是 ", mytable["wow"]) -- 释放变量 alternatetable = nil print("alternatetable 是 ", alternatetable) -- mytable 仍然可以访问 print("mytable 索引为 wow 的元素是 ", mytable["wow"]) mytable = nil print("mytable 是 ", mytable) fruits = {"banana","orange","apple"} -- 返回 table 连接后的字符串 print("连接后的字符串 ",table.concat(fruits)) -- 指定连接字符 print("连接后的字符串 ",table.concat(fruits,", ")) -- 指定索引来连接 table print("连接后的字符串 ",table.concat(fruits,", ", 2,3)) fruits = {"banana","orange","apple"} -- 在末尾插入 table.insert(fruits,"mango") print("索引为 4 的元素为 ",fruits[4]) -- 在索引为 2 的键处插入 table.insert(fruits,2,"grapes") print("索引为 2 的元素为 ",fruits[2]) print("最后一个元素为 ",fruits[5]) table.remove(fruits) print("移除后最后一个元素为 ",fruits[5]) -
模块:
模块就是一个封装库,lua由变量、函数等元素组成一个table,这个table只要暴露出去,就作为了一个模块。于是声明和导入其实就是基于table的
-- 文件名为 module.lua -- 定义一个名为 module 的模块 module = {} -- 定义一个常量 module.constant = "这是一个常量" -- 定义一个函数 function module.func1() io.write("这是一个公有函数!\n") end local function func2() print("这是一个私有函数!") end function module.func3() func2() end return module-- test_module.lua 文件 -- module 模块为上文提到到 module.lua require("module") print(module.constant) module.func3()与C结合:
Lua可以通过loadlib函数轻松加载由C而来的共享对象库
local path = "/usr/local/lua/lib/libluasocket.so" local f = loadlib(path, "luaopen_socket") -
元表(Metadata)
对表本身的操作就是元方法,元方法就存储在元表中
lua中有两个重要函数用来处理元表
- setmetatable(table,metatable)
- getmetatable(table)
常见的元方法:
-
_index
用来给table设置基础数据
当你通过键来访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的__index 键。如果__index包含一个表格,Lua会在表格中查找相应的键。
由于面向函数编程的自由性, _index可能是一个表,也可能是一个function,对于function而已,默认接受table名和key
Lua 5.3.0 Copyright (C) 1994-2015 Lua.org, PUC-Rio > other = { foo = 3 } > t = setmetatable({}, { __index = other }) > t.foo 3 > t.bar nil
mytable = setmetatable({key1 = "value1"}, { __index = function(mytable, key) if key == "key2" then return "metatablevalue" else return nil end end }) print(mytable.key1,mytable.key2)- _newindex
用来给table绑定异常处理的handler
当你给表的一个缺少的索引赋值,解释器就会查找__newindex 元方法:如果存在则调用这个函数而不进行赋值操作。
- 为表添加操作符
用来实现表层面的向量操作,通常会定义_add,_sub,_mod等操作
实例代码(剽窃自菜鸟教程)
-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用 -- 自定义计算表中最大键值函数 table_maxn,即返回表最大键值 function table_maxn(t) local mn = 0 for k, v in pairs(t) do if mn < k then mn = k end end return mn end -- 两表相加操作 mytable = setmetatable({ 1, 2, 3 }, { __add = function(mytable, newtable) for i = 1, table_maxn(newtable) do table.insert(mytable, table_maxn(mytable)+1,newtable[i]) end return mytable end }) secondtable = {4,5,6} mytable = mytable + secondtable for k,v in ipairs(mytable) do print(k,v) end- _call
使得一个table可以像函数一样被调用
当给table传入一个参数(而不是下标时),触发call方法;使得table类似也具有function特性
示例代码
-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用 -- 自定义计算表中最大键值函数 table_maxn,即计算表的元素个数 function table_maxn(t) local mn = 0 for k, v in pairs(t) do if mn < k then mn = k end end return mn end -- 定义元方法__call mytable = setmetatable({10}, { __call = function(mytable, newtable) sum = 0 for i = 1, table_maxn(mytable) do sum = sum + mytable[i] end for i = 1, table_maxn(newtable) do sum = sum + newtable[i] end return sum end }) newtable = {10,20,30} print(mytable(newtable))table确实很灵活,后续面向对象的思想完美契合table;其中或许和面向函数编程的思想暗合 —— 一个table只需要具有常规数据类型的属性和函数类型的属性 就是一个对象。
-
协同程序 & 多线程
lua本身并不支持多线程,单个lua虚拟机只能运行在一个线程下。如果想并行处理一些操作,必须为每个线程部署到独立的lua虚拟机上。而现场的成熟多线程库有:lanses和Effil,他们都试图隐藏多虚拟机的细节,并且实现不同虚拟机间的相互调用。
而线程与协同程序的主要区别在于:一个具有多个线程的程序可以同时运行几个线程,而每一时刻只能有一个协同程序在运行。协同程序有点像同步了的多线程,或者等待同一把锁的多线程。
-
Question1:Lua虚拟机为何如此设计:
设计为一个虚拟机默认只能在一个线程中运行,不符合我们传统Java程序的理解,但是符合脚本程序执行的需求 —— lua往往作为程序执行的下游(最后一公里),因此多线程通常由上游调用方完成;另一方面,lua设计为轻量级,易嵌入,因此虚拟机设计从简单出发,同时避免线程切换的复杂性,多线程下内存分配和回收的复杂性等。
-
-
Question2:不同的lua多线程的实现方式:
-
多线程库 lanses & Effil
思路是启动多个虚拟机,对上层隐藏多虚拟机的细节
-
云风的skynet 通过框架支持多线程 //此处不懂如何实现的
-
-
lua垃圾回收
自动化内存管理大致有两种方式:
- 引用计数
- 标记每个对象的被引用数量,当降为0后自动清理
- 垃圾收集
- 由垃圾回收器在某时刻完成对垃圾对象的收集
引用计数不管在哪里,第一个问题都是需要解决循环引用,众所周知python GC使用的就是引用计数,它解决的方式是引入部分标记清除(或者是是可达性分析方法)
- python里操作如下: 当内存不足时,触发标记清除 —— 由栈上变量引用的对象作为root,开始可达性分析,无法到达的对象即为要淘汰的对象。
- 具体操作过程:维护一个双向链表(我觉得可以叫脏对象链表),链表A用来存放可能存在循环引用的对象(那些容器类型,自定义类型有可能有循环引用,而基本类型,String不会有)。在标记阶段通过可达性分析将可达的对象都标记,在清理阶段,遍历上述链表,将没有标记过的清除这里很不确定,网上资料很乱很杂,还有很多谜语人,感觉需要找一本权威的书来
而lua没有使用引用计数,选择垃圾收集,官方给出的原因是:对于动态类型语言,使用引用计数会带来overhead的压力;
- 我对此的理解是:使用引用计数法天生的问题是 需要随着代码执行实时更新对象的引用,而代码的逻辑可能导致对象的引用数变化特别频繁;静态类型语言在编译期做了许多优化来去掉不必要的计数调整(例如C++使用许多inline函数消除掉RAII中的冗余操作),但是动态类型语言只能在运行时确定数据类型,这就导致重载状态下的函数无法推测到底要内联哪个函数(下面细聊);于是动态类型在运行时无可避免要承受大量计数调整的操作
- 具体来说:对于如下代码:
inline int add(Student a, Student b) { return a.age + b.age; } int main() { int result = add(alice, bob); return 0; }C++这类静态类型在编译时可能会通过内联转化成
int main() { int result = alice.age + bob.age; return 0; }代码是随便写的,大佬别在意细节
对于上面的代码,进入add后对alice和bob的引用增加1,调用age时又增加1,跳出add后将原本调用age的减去; 对于下面的代码,只需要在调用age时增加1的引用计数即可
换句话说:内联避免了函数调用的成本 而静态类型无法在编译时期内联优化,因为如果有多个重载的add,根据参数类型不同选择执行不同的代码,但动态类型语言在运行前做不到。
接下来就是对标记清除法的优化历程了
- Lua 5.0 之前,垃圾回收的逻辑简单直接 —— 每创建一个对象就垃圾回收一次,每次垃圾回收都一次性执行完成,过程中通过双色标记法。
- Lua 5.0 针对垃圾回收频率做了调整,当内存分配超过了上次GC后的两倍,就跑一次全量GC。
- Lua 5.1 支持了渐进式垃圾回收,原理就是三色扫描替换双色扫描
- Lua 5.2 推出分代GC 优化GC效果,但在5.3又删除了
- Lua 5.4 真正推出分代GC
- 引用计数
-
Lua 面向对象
据leader所说:lua中万物皆table,原因在于lua使用table来描述对象的属性
直接上示例代码
-- 元类 Rectangle = {area = 0, length = 0, breadth = 0} -- 派生类的方法 new function Rectangle:new (o,length,breadth) o = o or {} setmetatable(o, self) self.__index = self self.length = length or 0 self.breadth = breadth or 0 self.area = length*breadth; return o end -- 派生类的方法 printArea function Rectangle:printArea () print("矩形面积为 ",self.area) end思想:类就是对象
- 在lua中,类的定义就是一个table,其实就是具有默认值的一个对象。
- 如何根据这个类创建对象?需要从table层面提供一种方法 —— table的元方法 —— 给这个原始table设置一个new方法,由其返回一个新的tableB
- 如何确定tableB是原始table类产生的对象?需要在table层面表明这种关系的一种方式 —— table的元方法 —— 给tableB的_index绑定原始table,实现的效果就是tableB是原始table的实现/重写...
之前无法理解晖哥说 Java是一门很中庸的语言,如今看来 Java之外天外有天
-
-