lua快速入门 —— 协同程序

本文最后更新于:2020年2月4日 下午

概览:协同程序提供了一种协作式的多线程。每个协同程序都等于是一个线程。一对yield-resume可以将执行权在不同线程间切换。然而与常规多线程不同,协程是非抢先式的。

参照书籍:《Lua程序设计(第二版)》

Lua版本:Lua 5.3.5

协同程序

  • Lua将所有关于协同程序的函数放在了一个名叫”coroutine“的table中。

  • create函数用于创建一个协同程序,只有一个参数,为一个函数。最终返回一个thread类型的值。

    1
    2
    3
    > co = coroutine.create(function() print"hi" end)
    > print(co)
    thread: 000000000078eba8
  • 一个协同程序有四种状态:挂起(suspended)、运行(running)、死亡(dead)和正常(normal).创建一个协同程序后它将储于挂起状态,可以使用status函数来检查协同程序的状态。

    1
    2
    > print(coroutine.status(co))
    suspended
  • 使用resume函数来启动执行协同程序,并将其状态从挂起 –> 运行

    1
    2
    3
    4
    5
    6
    7
    8
    > coroutine.resume(co)
    hi
    true
    > print(coroutine.status(co))
    dead
    > coroutine.resume(co)
    false cannot resume dead coroutine
    >
  • 使用yield函数来让一个运行中的协同程序挂起。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    co = coroutine.create(function()
    for i=1,10 do
    print("co",i)
    coroutine.yield()
    end
    end)

    > coroutine.resume(co)
    co 1
    true
    > coroutine.resume(co)
    co 2
    true
    > coroutine.resume(co)
    co 3
    true
    > print(coroutine.status(co))
    suspended
    >

    从协程的角度来看,所有在它挂起时发生的活动都发生在yield调用中。当恢复协同程序的执行时,对于yield的调用才最终返回。然后协同程序继续它的执行,直到下个yield调用或执行的结束。

  • 正常状态:当协程A唤醒了另一个协程B时,协程A就处于一个特殊状态,既不是挂起状态(A无法继续执行)也不是运行状态(B在运行)。

  • Lua协同程序的机制:通过一对resume-yield来交换数据。在第一次调用resume时,并没有对应的yield在等待它,因此传递给resume的额外参数都将视为协同程序主函数的参数。

    1
    2
    3
    4
    5
    6
    7
    > co = coroutine.create(function(a,b,c)
    >> print("co",a,b,c)
    >> end)
    > coroutine.resume(co,1,2,3)
    co 1 2 3
    true
    >

    数据由yield传给resume。true表明调用成功,true之后的部分,即是yield的参数.

    1
    2
    3
    4
    5
    6
    co = coroutine.create(function (a,b)
    coroutine.yield(a+b,a-b)
    end)

    > print(coroutine.resume(co,20,10))
    true 30 10

    相应地,resume的参数,会被传递给yield。

    1
    2
    3
    4
    5
    co  = coroutine.create (function ()
    print("co", coroutine.yield())
    end)
    coroutine.resume(co)
    coroutine.resume(co, 4, 5) --> co 4 5

    最后一个,协同代码结束时的返回值,也会传给resume:

    1
    2
    3
    4
    co  = coroutine.create(function ()
    return 6, 7
    end)
    print(coroutine.resume(co)) --> true 6 7
  • Lua的协同称为不对称协同(asymmetric coroutines),指“挂起一个正在执行的协同函数”与“使一个被挂起的协同再次执行的函数”是不同的,有些语言提供对称协同(symmetric coroutines),即使用同一个函数负责“执行与挂起间的状态切换”.

    与对称的协同和不对称协同的区别不同的是,协同与产生器的区别更大。产生器相对比较简单,他不能完成真正的协同所能完成的一些任务。我们熟练使用不对称的协同之后,可以利用不对称的协同实现比较优越的对称协同。

协程实例-生产者消费者

生产者消费者涉及两个函数,一个负责不断的产生值,另一个负责不断地消费值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 两个函数大致的样子
function producer()
while true do
local x = io.read() --产生新的值
send(x) -- 发送给消费者
end
end

function consumer()
while true do
local x = receive() --从生产者接受值
io.write(x,"\n") -- 消费新的值
end
end

这里的问题在于如何将send与receive匹配起来,这是一个典型的**“谁具有主循环”(who-has-the-main-loop)**的问题。由于生产者与消费者都处于活动状态,他们各自具有一个主循环,并且都将对方视为一个可调用的服务。

而协调程序被称为是一种匹配生产者与消费者的理想工具,因为调用者与被调用者之间的resume-yield关系会不断颠倒。当一个协同调用yield时,并不会进入一个新的函数,取而代之的是返回一个未决的resume的调用。相似的,调用resume时也不会开始一个新的函数而是返回yield的调用。这种性质正是我们所需要的,与使得send-receive协同工作的方式是一致的。receive唤醒生产者生产新值,send把产生的值送给消费者消费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
)

在这种设计中,程序通过调用消费者来启动。当消费者需要一个新值时,它唤醒生产者。生产者返回一个新值后停止运行,并等待消费者的再次唤醒。这种设计称之为——“消费者驱动”。

过滤器filter

扩展上面的设计,实现过滤器

过滤器是一种位于生产者与消费者之间的处理功能,可用于对数据的一些变换。过滤器既是一个消费者又是一个生产者,它唤醒一个生产者促使其产生新的值,然后又将变换后的值传递给消费者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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()
for line=1,math.huge do
local x = receiver(prod)
x = string.format("%5d %s",line,x)
send(x)
end
end)
end

function consumer(prod)
while true do
local x = receive(prod) --获取新值
io.write(x,"\n") --消费新值
end
end

-- 运行代码
p = producer()
f = filter(p)
consumer(f)

--或者 sonsumer(filter(producer()))

-- 不过貌似无法运行

可能很自然的想到UNIX的管道(pipe)协同是一种非抢占式的多线程。管道的方式下,每一个任务在独立的进程中运行,而协同方式下,每个任务运行在独立的协同代码中。管道在读(consumer)与写(producer)之间提供了一个缓冲,因此两者相关的的速度没有什么限制,在上下文管道中这是非常重要的,因为在进程间的切换代价是很高的。协同模式下,任务间的切换代价较小,与函数调用相当,因此读写可以很好的协同处理。

用协同程序实现迭代器

我们可以将循环的迭代器看作生产者-消费者模式的特殊的例子。迭代函数产生值给循环体消费。所以可以使用协同来实现迭代器。协同的一个关键特征是它可以不断颠倒调用者与被调用者之间的关系,这样我们毫无顾虑的使用它实现一个迭代器,而不用保存迭代函数返回的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
-- 打印一个数组元素的所有的排列
function permgen(a,n)
n = n or #a --默认n为a的大小
if n <= 1 then
printResult(a)
else
for i=1,n do
a[n],a[i] = a[i],a[n] --将第i个元素放到数组末尾
permgen(a,n-1) -- 生成其余元素的排列
a[n],a[i] = a[i],a[n] -- 恢复第i个元素
end
end
end

function printResult(a)
for i=1,#a do
io.write(a[i]," ")
end
io.write("\n")
end

-- 调用
permgen({1,2,3,4}) --只传一个参数也可以,函数中会对第二个参数进行默认赋值操作

--[[
2 3 4 1
3 2 4 1
3 4 2 1
4 3 2 1
2 4 3 1
4 2 3 1
4 3 1 2
3 4 1 2
3 1 4 2
1 3 4 2
4 1 3 2
1 4 3 2
2 4 1 3
4 2 1 3
4 1 2 3
1 4 2 3
2 1 4 3
1 2 4 3
2 3 1 4
3 2 1 4
3 1 2 4
1 3 2 4
2 1 3 4
1 2 3 4
--]]

转化为迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function permgen(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] --将第i个元素放到数组末尾
permgen(a,n-1) -- 生成其余元素的排列
a[n],a[i] = a[i],a[n] -- 恢复第i个元素
end
end
end

--定义一个工厂函数
function permutations(a)
local co = coroutine.create(function() permgen(a) end)
return function() --迭代器
local code,res = coroutine.resume(co)
return res
end
end

-- for循环中便利
for p in permutations({"a","b","c"}) do
printResult(p)
end

permutations函数使用了lua中常用的模式:将一个对协同的resume的调用封装在一个函数内部,这种方式在非常常见,所以专门为此专门提供了一个函数coroutine.wrap。与create相同的是,wrap创建一个协同程序;不同的是wrap不返回协同本身,而是返回一个函数,当这个函数被调用时将resume协同。wrap中resume协同的时候不会返回错误代码作为第一个返回结果,一旦有错误发生,将抛出错误。

1
2
3
4
-- 使用wrap来重写permutations
function permutations(a)
return coroutine.wrap(function () permgen(a) end)
end

一般情况下,coroutine.wrap比coroutine.create使用起来简单直观,前者更确切的提供了我们所需要的:一个可以resume协同的函数,然而缺少灵活性,没有办法知道wrap所创建的协同的状态,也没有办法检查错误的发生。

非抢占式的多线程(non-preemptive)

对于非抢占式的多线程来说,只要有一个线程调用了阻塞(blocking)的操作,整个程序在该操作完成前,都会停下来。但对于大部分程序来说,这是无法接受的。

1
2
3
--未完

相关模块一直无法安装以及使用