【Lua进阶系列】协程
大家好,我是Lampard猿奋~~
欢迎来到Lua进阶系列的博客,今天和大家分享一下lua中关于协程的知识点
(一)什么是协程
Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它几乎一切资源。
一个多线程程序可以同时并行运行几个线程,而协程却需要彼此协作地运行,并非真正的多线程。即一个多协程程序在同一时间只能运行一个协程,并且正在执行的协程只会在其显式地要求挂起(suspend)时,它的执行才会暂停。
Lua中的协同程序有点类似于在等待同一个线程锁的多线程程序。
(二)协程&线程的作用:
一开始大家想要同一时间执行那么三五个程序,大家能一块跑一跑,于是就有了并发。
但是一块跑就有问题了。我计算到一半,刚把多次方程解到最后一步,你突然插进来,我的中间状态咋办,我用来储存的内存被你覆盖了咋办?所以跑在一个cpu里面的并发都需要处理上下文切换的问题。进程就是这样抽象出来个一个概念,搭配虚拟内存、进程表之类的东西,用来管理独立的程序运行、切换。
后来一电脑上有了好几个cpu,好咧,大家都别闲着,一人跑一进程。就是所谓的并行。
有的时候碰着I/O访问,阻塞了后面所有的计算。空着也是空着,老大就直接把CPU切换到其他进程,让人家先用着。当然除了I\O阻塞,还有时钟阻塞等等。一开始大家都这样弄,后来发现不成,太慢了。为啥呀,一切换进程得反复进入内核,置换掉一大堆状态。进程数一高,大部分系统资源就被进程切换给吃掉了。后来搞出线程的概念,大致意思就是,这个地方阻塞了,但我还有其他地方的逻辑流可以计算,这些逻辑流是共享一个地址空间的,不用特别麻烦的切换页表、刷新TLB,只要把寄存器刷新一遍就行,能比切换进程开销少点。
比如在进程里面写一个逻辑流调度的东西,碰着i\o我就用非阻塞式的(比如在加载资源时,我们可以做一些初始化场景的逻辑)。那么我们就避免反复系统调用,还有进程切换造成的开销,分分钟给你上几千个逻辑流不费力。这就是协程。
(三)Lua中协程的调用
Lua语言中协程相关的所有函数都被放在全局表coroutine中。
(1)coroutine.create()
我们可以简单的通过coroutine.create()来创建一个协程,该函数只有一个参数即协程要执行的代码的函数(函数体body),然后把创建出来的协程返回给我们。
举个栗子:
local coA = coroutine.create(function()
print("hello coroutine")
end)
print(type(coA)) -- 输出字符串thread
(2)coroutine.status()
一个协程有以下四种状态:既挂起(suspended),运行(running),正常(normal),死亡(dead)。我们可以通过coroutine.status()来输出协程的当前状态。
当协程被create出来时,它处在挂起的状态,被唤醒之后会处在运行状态,当协程A唤醒协程B时协程A从运行转换成正常状态,当唤醒成功之后转化为挂起状态。而当整个协程体执行完毕的时候,协程处于死亡状态。
我们可以在coroutine表中找到相关定义
(3)resume和yeild
resume和yeildr的协作是Lua协程的核心,coroutine.resume()把挂起态的协程唤醒,第一个参数是唤醒的协程,之后的参数传给协程体和作为yeild的返回值。coroutine.yeild()把运行态的协程挂起,参数作为resume的返回值。
经典生产者消费者例子:
创建一个生产工厂,让它生产10件产品,每生产一件就把协程挂起,等待客户下一次提交需求的时候才重新resume唤醒
local newProductor
-- 生产者
function productor()
local i = 0
while true do
i = i + 1
send(i) -- 将生产的物品发送给消费者
end
end
-- 消费者
function consumer()
local i = receive()
while i <= 10 do
print("生产第"..i.."件商品")
i = receive()
end
end
-- 接受
function receive()
-- 唤醒程序
local status, value = coroutine.resume(newProductor) -- 第一个协程的状态为true时则证明唤醒成功
return value
end
-- 发送
function send(x)
coroutine.yield(x) -- yield的参数作为resume的返回值
end
-- 创建生产工厂
newProductor = coroutine.create(productor)
consumer()
测试结果:
以上的设计称为消费者驱动式的设计,其中生产者是协程,消费者需要使用时才唤醒生产者。同样的我们可以设计消费者为协程,由生产者唤醒的生产者驱动设计。
(四)通过协程实现异步I/O
一般的同步IO(读取文件内容,逆序输出)
local t = {}
local inp = io.input("text.txt")
local out = io.output()
-- 读取文件中的内容
for line in inp:lines() do
t[#t + 1] = line
end
-- 逆序输出
for i = #t, 1, -1 do
out:write(t[i], "\n")
end
文件内容&输出结果
使用异步I/O的方式实现
首先,把读写循环的逻辑抽象出来。使用一个命令队列存放读写的逻辑,若未读取完毕则继续读取,若读取完毕,则进行输出。直至输出完所有的信息之后,则结束逻辑。
local cmdQueue = {}
local lib = {}
-- 读
lib.readLine = function(stream, callback)
local nextCmd = function()
callback(stream:read())
end
table.insert(cmdQueue, nextCmd)
end
-- 写
lib.writeLine = function(stream, line, callback)
local nextCmd = function()
stream:write(line)
callback()
end
table.insert(cmdQueue, nextCmd)
end
-- 停止
lib.stop = function()
table.insert(cmdQueue, "stop")
end
-- 执行
lib.runLoop = function()
while true do
local nextCmd = table.remove(cmdQueue, 1)
if not nextCmd or nextCmd == "stop" then
break
end
nextCmd()
end
end
return lib
然后,把整个读取的流程编写成一个协程。其中每次进行读写时把协程挂起,当读写完一行之后,则通过回调重新唤醒,从而实现异步I/O。输出的结果是一样的,但若碰到IO阻塞,我们就可以愉快地调用其他协程避免阻塞啦
local lib = require "asyncLib"
-- 创建协程
local run = function(func)
local coFunc = coroutine.wrap(function()
func()
lib.stop()
end)
coFunc()
lib.runLoop()
end
local putline = function(stream, line)
local co = coroutine.running()
local callback = function()
coroutine.resume(co)
end
lib.writeLine(stream, line, callback)
coroutine.yield()
end
local getLine = function(stream)
local co = coroutine.running()
local callback = function(line)
coroutine.resume(co, line)
end
lib.readLine(stream, callback)
local line = coroutine.yield()
return line
end
-- 调用
run(function()
local t = {}
local inp = io.input("text.txt")
local out = io.output()
while true do
local line = getLine(inp)
if not line then break end
t[#t + 1] = line
end
for i = #t, 1, -1 do
putline(out, t[i].."\n")
end
end)