1 Star 0 Fork 11

coder_lw / wiki

forked from deepinwiki / wiki 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
lua 语言入门.md 29.46 KB
一键复制 编辑 原始数据 按行查看 历史
htqx 提交于 2023-03-03 14:06 . 表展开

lua 语言入门

前言

为什么需要学习 lua 语言。 lua 是一个脚本语言,它和 js 这种 html 默认支持的脚本语言比有什么优势?

我感觉是 lua 比 js 更加小巧简单。js 其实算是比较复杂的脚本语言,它的思想是参考同样很累赘的 java。这类庞杂语法的语言,在现在这个时代,变得没有那么受欢迎了。不过 js 默认是浏览器支持,这点优势无比巨大。但是浏览器 wasm 这个通用字节码方向出来后,其他脚本语言其实也有在浏览器作为前端开发的前景。

lua 自身的简练,说不定将来会是 js 的有力竞争对手。 lua 传统上是作为游戏开发的嵌入式语言,它和 c 库有很好的整合能力。对于本地应用来说,相较 js 是传统优势项目。

和本地的脚本语言,如 bash 之类的比较,lua 更加正式,更像一个传统的编程语言,带有不错的标准库,执行效率在脚本语言中几乎是最高效的。和 python 比较,虽然没有 python 那么庞大的第三方库,但 lua 的优势还是精炼和小巧,性能也比较突出。本来,用脚本语言为啥会搞那么多庞大的库也是比较奇怪的事。这种大工程难道不应该用更严肃的语言,比如 go 之类的编写么,所以 python 的发展也有点疲态。

在嵌入式 iot 场合,lua 也有一些不错的支持。

总结,lua 的优势就是它小,高性能,和 c 库底层结合好,能做好脚本的份内工作。

环境配置

# 安装后可以这样运行一条 lua 语句
lua -e "print(utf8.len('你好'))"

lua prog.lua # 运行文件
#!/usr/bin/env lua

lua # 交互式,非常适合用来学习语法
dofile("lib1.lua") # 在交互式模式下,运行文件

语法

lua 是一个动态语言,它对类型的要求不是那么严格。优点是编程便利,缺点容易出现数值和字符串自动转换之类的错误。尤其比较关键的算法,还是要特别仔细的检查数据类型。

注释:

  1. -- 单行注释
  2. --[[多行注释]]

运算符:

  1. ^ : 幂运算,左结合。 -x^2 == -(x^2)
  2. .. : 字符串拼接
  3. # : 长度
  4. ~= : 不等
  5. a,b = b,a : 多重赋值

整数:64 位有符号数。

浮点:双精度。精度53位整数。

字符串: 不可变类型。

  1. [[多行字符串]]
  2. \z : 在字符串源代码上跳过空白。(为了书写多行字符串)
  3. utf8.len() : 替代 string.len(),能正确处理中文。等等

类型转换:

  1. "10" + 1 == 11.0
  2. tonumber("3") == 3
  3. tonumber("10e4") == 10000.0
  4. tonumber("10e") == nil
  5. not nil == true
  6. 3.3 | 0 == 3

类型:

  1. nil : 空
  2. boolean : 布尔。 (比较只将 nil 和 false 视为假)
  3. number : 整数和浮点
  4. string : 字符串
  5. userdata : 用户数据
  6. function : 函数
  7. thread : 线程
  8. table : 表

唯一的数据结构,可表示数组、集合、记录、包(package)、对象等。表是一个指向动态内存的指针,由运行时负责资源管理。

a = {}  -- 空表
a["hi"] = 123 -- 随意扩充成员
a[42.0] = "hello" -- 索引是任意非空类型
a[42] = "hi" -- 作为键时,数值会自动转换成整型或浮点,即 42.0 => 42
print(a.hi) -- 123

-- 列表式(从 1 开始索引)。数组
b = {"a","b","c"}
-- 记录式
c = {x=7, y=8, z=9}
-- 特殊字段名
d = {["+"] = "add", ["-"] = "sub"}

遍历:

t = {1,2,nil,3}

for k,v in pairs(t) do -- 获取键 k、值 v,另外 ipairs() 按顺序索引
    print (k, v)
end

for k = 1, #t do -- 空洞(hole)也会被读取,即 t[3] = nil
    print(k, t[k]) -- 我认为这是一个设计错误,因为 nil 表示不存在
end

安全访问:

例: a.b 如果 a 本身是 nil ,那么就是不安全的访问操作。

解决这个问题的思路:给出不满足时该做什么。而不是只想着满足时做什么,这样可能会导致潜在的逻辑错误。

other = { b=1 }
-- or:当 a 是 nil 时,返回 other
x = a or other -- 给出替代品,或中断报错
c = x.b 

-- c = a.b.c.d
for i=1, 1 do
    if not a then break end -- a
    if not a.b then break end -- a.b
    if not a.b.c then break end -- a.b.c
    c = a.b.c.d
    print(c)
end

深入理解表

因为表是 lua 中的对象,它甚至相当于其他语言各种语言要素的集合体,所以必须要理解如何在 lua 中表达其他语言的概念。

-- 数组:从 1 开始
a = {1,2,3,4} -- a[0] = nil, a[1] = 1, a[2] = 2...
a[0] = 0 -- 将会添加 k = 0, v = 0 的新键值对,但并不会被认为是数组的一部分

-- 对象:表就是一个对象,键值对,如果值为 nil ,该键值对就被视为不存在
a[0] = nil -- 删除该键值对
a[0] = function() return 1 -- 元素可以是任意类型,甚至是另一个表
b = a -- 对象是指针,所以他们是在操作同一个对象,同时改变
-- 克隆对象是复杂的操作,需要对对象模型深入理解
-- 1. 展开对象的成员,获得键值对
-- 2. 设置新对象的键值对 = 该键值对
-- 3. 当该建是对象时选择是否克隆(包括键和值都可能是对象)
-- 相关技术
b = {table.unpack(a)} -- 展开成值的序列,对一般数组而言足够
b = {}
for k,v in pairs(a) do b[k] = v end -- 获得键值对,并设置键值对,但不对键值对是对象的时候做任何处理
-- 如果不是数组或者键值对的复制,而是真正的对象的复制,那么应该由对象自身实现拷贝函数更为合理,而不是用外部的观点去实现对象的复制,增加的部分可能是对成员为对象时的处理,和对象的原型(基类)的处理

-- 多重数组
-- lua 缺乏内置的表操作函数,得靠 for 自己展开

函数

lua 中的函数可以接收多于,或者少于参数声明个数的参数。

function f(a, b) -- 函数名 f,参数 a,b
    local c = a + b -- 局部变量
    return c, a, b -- 返回值
end -- 函数结束

-- 位于最后参数的调用返回值,将会自动扩展
f(1, f(1,2)) -- f(1, 3, 1, 2) --> 4,1,3

可变长参数函数:

-- ... 是可变长参数表达式
function g(...) -- 可变长参数函数
    local result = {}
    -- 函数调用参数是列表或字符串时可以省略括号
    for _,v in pairs{...} do -- {...} 转换为列表
        table.insert(result, v) -- 将新元素插入表后端
    end
    return table.unpack(result) -- 将列表转化为可变长参数表达式
end

print( g(1,2,nil,3) ) -- 过滤掉 nil 元素

当函数内部最后是返回时调用其他函数,那么该函数的栈可以提前释放,因而不再占用栈空间。这叫尾调用优化。对于满足这种条件的递归函数来说,它将可以无限递归,而不会用尽栈空间。

function f(a)
    if a > 1 then 
        return f(a-1)  -- 尾递归
        -- 注意必须在 return 之后,且没有任何操作
    end
    return a
end

f(1000000000.4)  -- 不会栈溢出

输入输出

-- io.stdin 默认输入流
-- io.stdout 默认输出流
-- io.stderr 默认错误流
file = io.read("a") -- 完整读取文件内容
-----------------
lines = {}
for line in io.lines() do -- 按行读取
    table.insert(lines, line)
end

for k,line in ipairs(lines) do
    -- 写入(可变长参数)
    -- string.format 格式化
        -- %0 填充0,-3宽度3,左对齐,d 整数
    io.write(string.format("%0-3d", k),line,"\n") 
end
------------------
-- file 流
-- err 失败错误
-- errnum 错误码
-- "r" 只读
filename = "/home/htqx/code/main.zig"
file, err, errnum = io.open(filename, "r")
if err then print(errnum,err) end
lines = file:read("a") -- 读取流
print(lines)
file:close() -- 关闭流

file = io.open(filename,"r")
io.input(file) -- 设置当前输入流
io.read() -- 读取一行
io.input(io.stdin) -- 设置回默认,标准输入流
file:close()  
-------------------
file = io.popen("ls", "r") -- 执行命令并返回流
a:read()
file:close() -- 结束流和命令, 返回: true, "exit", 0

控制流程

-- 分支语句
if not a then -- a == nil
    a = 1
elseif a > 1 -- a ~= nil and a > 1
    a = 0
else -- a ~= nil and a <= 1
    a = 2
end
------------------
-- 循环语句
a = {1,2,nil,3}
local i = 1
while a[i] do
    print(a[i]) -- 1,2
    i = i + 1
end

local i = 1
repeat
    print(a[i]) -- 1,2
    i = i + 1
until not a[i] -- 为假时循环。且第一次总是运行的

for i=1, #a, 1 do  -- 索引,目标值,步进
    print(a[i])
    i = i + 1
end
------------------
-- goto 语句
a = {0,-1,1,2,nil,3}
i = 1
while a[i] do
    ::redo:: -- 只能位于局部变量前
    local j = i
    i = i + 1
    if a[j] == nil or a[j] == 0 then goto continue end
    if a[j] < 0 then goto redo end
    print(a[j])
    ::continue::  --最后一条除外
end

闭包

闭包有点类似匿名函数。在 lua 中,函数其实都是匿名的。它只是被赋予到一个同名的变量。

function f(x) return x*x end  
f = function(x) return x*x end -- 等价

local function f(x) return f(x-1) end -- 局部函数
local f; f = function(x) return f(x-1) end; -- 等价

xim = {}
function xim.f(x) end 
xim.f = function(x) end -- 等价

闭包:闭包主要特征是可以捕获外层函数的变量。lua 函数实际都是闭包。

function newCounter()
    local count = 0
    return function() -- 闭包
            count = count + 1  -- 捕获外层变量 count
            return count
        end
end

c = newCounter()  -- 被捕获的变量已经和 newCounter 无关,和 c 绑定
d = newCounter()  -- 新闭包
print(c(),c(),c(),d()) -- 1,2,3,1

高阶函数:函数参数是函数

-- 导数公式: (f(x+d) - f(x)) / d, d 为无穷小
function g(f,d,...)-- 返回指定函数的导数
    local x = {...}
    d = d or 1e-4 -- 默认值
    for i = 1, #x do
        if not x[i] then goto continue end
        x[i] = x[i] + d
        ::continue::
    end

    x = {f(table.unpack(x))}
    local y = {f(...)}
    for i = 1, #x do
        if not x[i] or not y[i] then goto continue end
        x[i] = (x[i] - y[i]) / d
        ::continue::
    end
    return table.unpack(x) 
end 

g(math.sin, nil, 5.2) -- 导数的近似值
math.cos(5.2) -- sin 的反函数是 cos
-----------------------
-- 对原函数参数的包装
function gen2(f,b)
    return function(a,...)
            return f(a,b,...)
        end
end

gd = gen2(g, 1e-9) -- 对 g 进行包装,避免第二个参数 d 的使用
gd(math.sin, 5.2)
-----------------------
-- 柯里化:将多参数函数转为多个单参数函数
function currying(f,n,...)
    local args = {...} 
    if n <= 0 then return f(...) end -- 调用原函数

    return function(d)
        table.insert(args, d) -- 添加到原参数后面
        -- unpack 必须在最后才能扩展
        return currying(f,n-1,table.unpack(args))
        end
end

currying(math.min, 3)(5.2)(1)(-10) -- 柯里化
math.min(5.2, 1, -10) 

字符串匹配

有人说,编程的很大一部分内容,就是对字符串进行处理。确实,大部分编程语言都会内置强大的字符串处理函数。

  1. string.find(字符串,模式匹配,bool) : bool == true 是简单匹配
  2. string.match(字符串,模式匹配)
  3. string.gmatch(字符串,模式匹配):多次
  4. string.gsub(字符串,模式匹配,替换内容,次数)

模式字符:

  1. .: 点,代表任意字符
  2. %a : 字母
  3. %c : 控制字符
  4. %d : 数字
  5. %g : 非空格
  6. %l : 小写字母
  7. %p : 标点符号
  8. %s : 空白
  9. %u : 大写字母
  10. %w : 字母和数字
  11. %x : 十六进制数字
  12. %b() : "(" 到 ")" 之间的字符,可替换为其他两个字符
  13. %f[] : 前置约束集合

以上大写为它的补集。

  1. + : 1 - n
  2. - : 0 - n (最小匹配)
  3. * : 0 - n
  4. ? : 0 - 1
  5. ^ : 补集、开头
  6. $ : 结尾
  7. [] : 集合
    1. [0-7]: 0 - 7 的字符集合
  8. () : 匹配组
    1. %0 : 整个匹配
    2. %1 : 第一组
    3. () : 空捕获,返回位置
  9. % : 转义

以上基本是最大匹配。

str = "Nns110snN"
-- 单次和多次匹配的区别
string.match(str, "[01]+")  -- 110
for v in string.gmatch(str, "[sn]+") do
    print(v) -- ns,sn
end
-- 最小匹配和最大匹配的区别
string.match(str, "n.-s") -- ns
string.match(str, "n.*s") -- ns110s
-- 开头、结尾
string.match(str, "^.?.?") -- Nn
string.match(str, ".?.?$") -- nN
-- 范围匹配和前置匹配
string.match(str, "%bss") -- s110s
for v in string.gmatch(str, "%f[%l][ns]+%f[%u]") do
    print(v) -- sn
end

前置匹配(Frontier pattern)的逻辑:当前字符满足[%l],且前一个字符不满足。这有点类似回顾查找否定式(lookbehind),即 (?<![])。看上去简单,实际使用起来还是有点绕的。

比如你想捕获 [ns], 先 [ns]+,然后前面只要不是小写即可 %f[%l],否定式。后面是大写 %f[%u],肯定式。为什么?前面是否定形式的,因为当前字符是[ns]+,后面是肯定式,因为当前字符是[ns]+后了。这就是前置后置条件的写法差异。注意前中后的关系,必然是变化的,否则怎么提取呢?

前置匹配模式就是让你可以对前后的差异做一些限定,因为默认的限定只是[^ns]

str = "Nns110snN"
-- 匹配组,返回每组括号匹配的内容
string.match(str, "(%a+).-(%a+)") -- Nns,snN
-- %1 引用第一个匹配组的结果,即 n 
string.match(str, "(%l)(.-%1)")  -- n, s110sn
-- 格式化字符串
string.gsub(str, "(%a+)(.-)(%a+)", [[
<%1 size="%2">
  <%3 />
</%1>]])
-- 替换为表对应项的值
string.gsub(str, "%a+", {Nns = 111, snN="000"}) -- 111110000
-- 用闭包处理结果
string.gsub(str, "%a+", function(s)return string.upper(s);end) 

数据结构

数据结构是算法的着力点,算法是问题的编程方法。方法高低(除了正确性)主要是看执行效率(空间和时间上的)。因此对于稍微复杂的问题来说,构造适当的数据结构是很关键的。

数据结构和算法的知识,是高于具体编程语言的,所以反过来用某个编程语言去实现经典数据结构和算法,对熟练这个语言也是极好的帮助。

数组遍历内层尽量对应低维度。如a[i][k] 对应 for i..for k.. 。用 pair 代替索引,可以对稀疏数组进行性能优化。

字符串拼接符(..)是低效率的,使用表({})来做缓冲,并使用 table.concat 来连接。

-- 单链表
list = {next = list, value = v}
-- 双端队列
queue = {first = 0, last = -1}
-- 反向表
days = {"一", "二", "三", "四", "五", "六", "日"}
revDays = {}
for k,v in pairs(days) do
    revDays[v] = k
end
revDays["日"] -- 7
-- 图
node{name, adj = node}
graph["name"] = node

序列化

将语言内部的对象(结构和值),存储到文件中。序列化有多种编码方式。如二进制存储(高效),或文本存储(易读易改)。

模块

现代语言都会包含模块。模块对程序的功能进行工程角度的逻辑划分。

-- 加载 mod.lua
-- 搜索路径 package.path
-- C 搜索路径 package.cpath
-- loadfile 加载
-- loadlib 加载 C 库
-- ./m.lua or ./m/init.lua
local m = require "mod" 

定义一个模块:

-- package.loaded
-- package.preload
-- 子模块 M.b 的默认路径 m/init.lua m/b.lua
local M = {}
local function M.new(r,i)
    return {r=r,i=i}
end
return M

迭代器

迭代器本质是返回一个闭包。该闭包每次调用将返回下一个元素,配合 for...in 语句,就能循环直到返回空。

pairs 能返回一个表的迭代器。

next(t, i) 内置函数是 pairs 函数的关键,将返回 t 表 i 键的下一个元素 k,v(按内部固有顺序),如果 i 为 nil 将返回第一个元素。

-- 将表转换为迭代器
for k,v in pairs({...}) do
end
-- 实现一个范围迭代器
function rang(xend, start, step)
    local start = start or 1 -- 提供默认值
    local xend = xend or 1
    local step = step or 1
    local index = start -- 作为闭包状态
    return  function() 
        local i = index
        index = index + step -- 修改捕获的变量
        if i <= xend then 
            return i
        end 
    end
end
-- for...in 获取闭包,调用闭包,直到它返回 nil,...
-- 一次最多只能保存接受三个闭包返回值
for i in rang(5) do
    print(i)
end

-- 累计器,具有初始值和累计值
-- 该闭包没有绑定环境变量,是无状态(只用参数)迭代器
function sum(iter, start)
    local start = start or 0
    return function(s,var) -- 生成器、累计值
        local next = s()
        if not next then return end
        return next + var -- 累计值
    end, iter, 0 -- 函数、生成器、累计值
end
for s in sum(rang(5)) do
    print(s)
end

元表和元函数

元函数能实现对内置运算符和特殊函数的重载。

元表是原型的基础:当表找不到相关操作,就会从元表中查找原函数 __index,从而获取在元表中定义的操作,因此元表就成了当前表的原型(类似 js 语言)。

原型实现了面向对象的继承操作,但元函数本身不会被继承。

  1. __tostring : tostring()
  2. __pairs : pairs()
  3. __metatable : 对元表进行保护
  4. __index : 读取下标,即 self[k]。只有不存在的下标时才会调用函数,如果要任意下标都调用,那么得用代理模式,将目标对象封装到内部。
    1. rawget : 原方法
  5. __newindex : 设置下标,即 self[k] = value
    1. rawset
  6. __len : "#"
  7. __mode : 弱引用。等于 "kv" 时键和值都是弱引用
  8. __gc : 析构器
-- 获取元表
-- 内置的字符串类型具备元表对象
getmetatable("")
---------------------
-- 设置元表
-- 元表本身也是普通的表
t = {v=1}
a = {} -- 元表
-- + 的元方法
a.__add = function(self, right)  -- 添加元方法
    local left = { v = self.v}  -- 复制 self
    setmetatable(left, getmetatable(self)) -- 含元表
    left.v = left.v + right.v
    return left
end
-- 转字符串的元方法
a.__tostring = function(self) return self.v end
setmetatable(t, a) -- 设置 t 元表为 a
print(t + {v=2}) -- 3

面向对象

面向对象让对象可以使用相同的一组接口。lua 使用原型来实现面向对象。

原型条件:

  1. setmetatable(t, a) : 设置 a 为 t 原表
  2. a.__index = a : 元表设置索引字段
a = {}
aa = {__tostring = function()return "a{}" end}
setmetatable(a, aa)
function a:say() -- 相当于 a.say(self)
    print(self) -- 隐藏的第一个参数 self
end
a:say() -- 相当于 a.say(a)
b,a = a,nil
b:say()

-- 原型(class)
c = {}
b.__index = b  -- 设置 __index 为原始函数
setmetatable(c, b)
c:say() -- 继承了b(即 a 的更名)的接口
-- 但因为 b 没定义元函数 __tostring (在 aa 上,但元函数不能继承)
-- 所以输出默认的 table:0xnnnn...
function aa:hi()
    local print = io.write
    print(type(self),"{")
    for k in pairs(self) do
        print(k,", ")
    end
    print("}\n")
end
aa.__index = aa -- aa 变更为原型
c:hi() -- c --> b --> aa:hi()

协程

协程是异步执行任务的技术。在业务活动中,很多时候需要我们同时执行多个任务。

协程(coroutine)互动关系:

  1. create(f) --> co : 协程对象(状态)
  2. resume(a,b) --> f(a,b) : 第一次调用方法 f,之后对应每个 yield
  3. yield(a,b) --> resume result : yield 的参数是resume 的返回值
  4. resume(a,b) --> yield result : resume 的参数是 yield 的返回值

resume 调用协程,yield 返回调用处,直到协程终结。

协程看上去像是一个交互式的麻烦一点的函数调用。但它能作为事件驱动、协作式多线程、构造式迭代器等目标的基础。

协程定义了四个状态:

  1. 挂起 suspended : 创建和遇到 yield 时
  2. 运行 running : 执行闭包时
  3. 正常 normal : 其他情况(如切换到另一个协程时)
  4. 死亡 dead : 执行完毕时
co = coroutine.create(function(s) -- suspended
print("task1:", s) -- running
local r = coroutine.yield("2.do...") -- suspended
print(r) -- running
end) -- dead
print(type(co)) -- thread

_,v = coroutine.resume(co, "1.begin") 
print(v) -- 2.do...
coroutine.resume(co, "3.end")

异步模式

模式的意思是前人总结出来的设计经验。它不是硬性要求,但值得借鉴。

  1. producer - consumer : 生产消费模式
    1. consumer-driven : 消费者驱动(producer 是协程)
    2. producer-driven : 生产者驱动
  2. producer - filter - consumer : 管道过滤器模式
function producer(s)
    for i=1, s do
        send(i)
    end
end
function consumer(task)
    local value = receive(task)
    while value do
        print(value)
        value = receive(task)
    end
end
function send(x) -- 发送
    return coroutine.yield(x) -- 消费驱动,因为它暂停了生产者
end
function receive(task)  -- 接收
    local _, value = coroutine.resume(task)
    return value
end
-- 配置
prod = coroutine.create(function() return producer(10)end)
-- 启动
consumer(prod)
-------------------------------------
-- 管道过滤器
function filter(task)
    return function()  -- 返回闭包更方便
        local value = receive(task)  -- 既是消费者
        while value do
            send(value * value)  -- 又是生产者
            value = receive(task)
        end
    end
end

prod = coroutine.create(function() return producer(10)end)
filter = coroutine.create(filter(prod))
consumer(filter)

生成器

协程对于许多程序员来说是一个比较新的概念,所以有必要了解一下其中的细节。

协程保持状态,并允许暂停,当下次进入时,只能从暂停位置继续。

lua 的协程是非对称协程(存在调用和被调用者),对称协程两者是平等的。被调用者只会回到调用者(主协程)。

而对称协程会切换指定协程。只有单一操作 yield to xxx,没有 yield - resume 成对操作。

对称协程的自由度,导致调用者和被调用者的角色不明确,也导致需要附加的手段来保持信息传递。因此非对称协程更易于编程。

lua 的协程是基于堆栈和第一类(first class)构造的完全协程(指没限制的)。

-- 生成式迭代器
function rangIter(s)
    local co = coroutine.create(function() return producer(s)end)
    return function() 
            return receive(co)
        end
end

for i in rangIter(10) do
    print(i)
end

事件驱动

-- 事件驱动
-- 典型的事件驱动框架,用队列来存储事件,用回调函数来处理事件
cmdQueue = {}
function read(stream, callback)
    table.insert(cmdQueue, function() callback(stream:read()) end)
end
function write(stream, line, callback)
    table.insert(cmdQueue, function() callback(stream:write(line)) end)
end
function stop()
    table.insert(cmdQueue, "stop")
end
function runloop()
    local nextCmd = table.remove(cmdQueue, 1)
    while nextCmd do
        if nextCmd == "stop" then break end
        nextCmd()
        nextCmd = table.remove(cmdQueue, 1)
    end
end
-- 任务:读取多行文本,然后逆序输出
t = {}
function lines(line)  -- 事件处理
    if line then
        t[#t + 1] = line
        read(io.input(), lines)
    else
        index = #t
        printlines()  -- 读取完毕,产生输出事件
    end
end
index = 0
function printlines()
    if index < 1 then return print() end
    write(io.output(), t[index].." ", printlines)
    table.remove(t, index)
    index = index - 1
end

read(io.input(), lines) -- 产生第一个事件
runloop() -- 执行事件循环
------------------------------------------------
-- 协程版事件驱动
function readline(stream)
    local co = coroutine.running() -- 获取当前协程
    local callback = function(l) coroutine.resume(co, l) end -- 恢复
    read(stream, callback) -- 让主循环调用回调来恢复当前函数
    local line = coroutine.yield() -- 获取stream:read()的值
    return line
end
function writeline(stream, line)
    local co = coroutine.running()
    local callback = (function() coroutine.resume(co) end)
    write(stream, line, callback)
    coroutine.yield()
end
-- 业务代码看上去和同步一样,易于理解
function run() 
    local t = {}
    local line = readline(io.input())
    while line do
        t[#t + 1] = line
        line = readline(io.input())
    end
    for i = #t, 1, -1 do
        writeline(io.output(), t[i].." ")
    end
    print()
    stop()
end
co = coroutine.create(run)
coroutine.resume(co)
runloop()

事件驱动模型,如果按照传统的方法编程,会比较难理解。因为它让程序员基于”产生事件,回调事件处理函数"的思维来编程,有种认知负担在里面。而使用协程后,业务代码变得很清晰,和一般性的同步代码没有区别,细节都隐藏在背后了。

程序有点复杂,下面描述一下究竟干了什么:

  1. 业务 run 是 co 协程,它第一次 resume 将会将读取的事件写入事件队列。这时候事件并没有执行。而读取函数 readline 是一个异步函数,它暂停了。
  2. 这时候控制权回到主线程,执行 runloop 这个事件循环。这时候发现里面有一个事件,调用它的回调函数 callback(stream:read()) 将暂停了的 readline 重新激活,并将读取的第一行返回给 local line = coroutine.yeild()。
  3. 然后函数执行完毕,这时候控制权去哪里了?并不回到事件主循环,即 runloop,而是将去到 run() 里面的 line = readline(io.input())。
  4. 然后又继续,直到遇到下一个 yield 才回到 runloop,多次同样的剧情直到所有事件回调都执行完,runloop结束,程序结束。

协作式多线程

协程本质就是就协作式多任务,resume - yield 相互配合将函数分隔成若干小任务,从而能穿插另一个任务。而系统现有的多任务方案(系统线程),是抢占式的,cpu 间隔到了,就轮转到另一个任务,任务之间顺序是随机的。

可能会有人对协作式产生疑问,因为它的顺序都是确定的,感觉不像是多任务一起干,感觉不像同时进行。这其实和任务本身的性质有关。

任务与任务之间如果是有先后顺序的,那么必然需要进行同步,就算线程环境也是要等待的。

如果任务之间没有先后顺序,那么就可以随机执行。那么能不能用协程实现这种随机执行的效果呢?

-- 协作式多任务
pool ={}

function with(...)
    for _,v in pairs({...}) do
        table.insert(pool, v)
    end
end
-- 调度器
-- 改进的方法是 pool 装消费者,而不是随意重启生产者
function runPool()
    while #pool > 0 do
        local index = math.random(#pool)
        local co = pool[index]
        if coroutine.status(co) == "suspended" then
            coroutine.resume(co)
        else
            table.remove(pool,index)
        end
    end
end

-- print range
function range(a,b)
    local co = coroutine.create(function()
        for i =a ,b do
            print(i)
            coroutine.yield() --如果用 coroutine.wrap 需要传参
        end
    end)
    return co
end

with(range(1,2), range(3,6), range(7,10)) -- 无关任务
run = coroutine.wrap(runPool) -- 将协程包装成闭包
run()

以上代码运行良好,但是它其实也只是一个顺序被打乱的单线程程序。当某个节点被暂停,后续都需要等待(哪怕逻辑上他们是无关的任务)。要想实现真正的并行执行,必须借助系统的多线程。

但 lua 本身不支持多线程。原因是多线程太复杂,lua 只是一个基于标准 c 库设计的脚本语言。c 标准库没有定义线程。

虽然没有多线程,但是可以编写非阻塞的 api。

c 语言集成

lua 作为嵌入式语言,它提供库嵌入到第三方使用(c),反过来 lua 也能调用 c 编写的库。

// 编译时添加 liblua.so 即参数 -llua
#include <stdio.h>
#include <string.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"

void l_main(lua_State*);
int main(void){
    char buff[256] = "print('hello lua based on c')"; // lua 源码
    lua_State *lua = luaL_newstate(); // lua 状态机
    luaL_openlibs(lua); // 打开标准库
    luaL_loadstring(lua, buff); // 编译源码
    lua_pcall(lua, 0, 0, 0); // 执行
    //-------------
    // 测试 lua 调用 c 函数
    l_main(lua);
    //-------------
    fprintf(stderr, "%s\n", lua_tostring(lua, -1)); // 打印错误信息
    lua_close(lua); // 关闭 lua
    return 0;
}
//---------------------------------
// lua 调用 c 函数
int l_add(lua_State *); 
void l_main(lua_State* lua) {
    lua_pushcfunction(lua, l_add); // c 函数指针入 lua 栈
    lua_setglobal(lua, "myadd"); // 绑定到全局变量 myadd
    char buff[] = "print('myadd :', myadd(1,2))"; // 输出 3
    luaL_loadstring(lua, buff);
    lua_pcall(lua, 0,0,0);
}

long add(long a, long b) {
    return a + b;
}

int l_add(lua_State *lua) { // 包装函数
    long a = lua_tointeger(lua, 1); // 获取整数参数
    long b = lua_tointeger(lua, 2);
    long c = add(a,b);
    lua_pushinteger(lua, c); // 返回值
    return 1; // 返回值个数
}

参考

  1. 前置匹配:https://blog.csdn.net/hiheasy/article/details/7937831
  2. 协程切换上下文:https://zhuanlan.zhihu.com/p/463549090
  3. 协程对称和不对称:https://zhuanlan.zhihu.com/p/363775637
  4. 协程的定义与分类:https://blog.csdn.net/wuhenyouyuyouyu/article/details/52709395
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/coder_lw/wiki.git
git@gitee.com:coder_lw/wiki.git
coder_lw
wiki
wiki
master

搜索帮助