programming in lua (12)

第 9 章和第 30 章部分内容,都是讲线程相关的。

基本概念

lua 中的 coroutine 和我们通常说的“线程”概念相近,但是这里的 coroutine 并不是真正的线程,在某个时刻只有一个 coroutine 在执行。lua 将 coroutine 的所有函数都放在一个叫“coroutine”的 table 中,下面来看一下这些函数。

co = coroutine.create(function () print("hello") end)
print(co) -- thread: 0x11fa810
print(coroutine.status(co)) -- suspended
coroutine.resume(co) -- hello
print(coroutine.status(co)) -- dead

create() 接收一个函数作为参数,创建一个对应的 coroutine ,返回值是一个 thread 类型,通过 print() 的输出可以看到。一个 coroutine 有四种状态:suspended,running,dead 和 normal,初始时处于 suspended 状态。直到我们显式地调用 resume() 这个 coroutine 才会运行,这时它的状态变为 running,运行过后状态变为 dead。

co = coroutine.create(function ()
    for i=1, 3 do
        print("co", i)
        coroutine.yield()
    end
end)

ret = coroutine.resume(co) -- co  1
print(ret, coroutine.status(co)) -- true suspended
ret = coroutine.resume(co) -- co  2
print(ret, coroutine.status(co)) -- true suspended
ret = coroutine.resume(co) -- co  3
print(ret, coroutine.status(co)) -- true suspended
ret = coroutine.resume(co) --
print(ret, coroutine.status(co)) -- true dead
ret = coroutine.resume(co) --
print(ret, coroutine.status(co)) -- false dead

在作为参数的函数中调用了 yield() 后 coroutine 的状态变为 suspended,直到我们调用 resume() 后才恢复运行。resume() 运行在 lua 的保护模式中,如果匿名函数出错了并不会打印出错信息,而是返回 resume() 内部继续执行。第四次调用 resume() 的时候返回值是 true,因为此时 coroutine 停在 yield() 中,所以仍然有效。

当在一个 coroutine A 中唤醒(resume)另一个 coroutine B 时,很明显 A 不是处于 suspended 状态,但是我们也不能对 A 调用 resume(),而且此时处于 running 状态的是 B(因为某一时刻只有一个 coroutine 在运行),因此这时 A 的状态称为 normal。

可以通过 create() 把参数传递给匿名函数:

co = coroutine.create(function (a, b, c)
    print("co", a, b, c)
end)

coroutine.resume(co, 1, 2, 3)    --> co 1 2 3

yield() 会把传递给它的参数作为 resume() 的返回值:

co = coroutine.create(function (a, b)
    coroutine.yield(a + b, a - b)
end)

print(coroutine.resume(co, 10, 7)) --> true 17 3

这样可以把匿名函数的中间执行结果传递给外面。yield() 还能把多余的参数收集起来(这个有点费解):

co = coroutine.create(function ()
    print("co", coroutine.yield())
end)

coroutine.resume(co) -- suspended at yield()
coroutine.resume(co, 10, 7, 5) -- co 10 7 5

当然最后匿名函数的返回值也会作为 resume() 的返回值:

co = coroutine.create(function ()
    return 6, 7
end)

print(coroutine.resume(co)) --> true 6 7

lua 提供的 coroutine 可以认为是异步的,因为它可以被一个函数挂起,由另一个函数唤醒。另外一些编程语言提供的同步 coroutine 则是只能由同一个函数来控制(其它函数想控制它必须通过这个函数)。异步 coroutine 有时也被叫做 semi-coroutine,但是 semi-coroutine 通常是指那些只能在函数的主体(即当前调用栈是空的地方)被挂起的 coroutine。

生产者-消费者问题

一个经典的例子就是生产者-消费者问题,即有一个 coroutine 作为生产者不停地生产,另一个 coroutine 作为消费者不停地消费生产者生产出来的内容。例如一个生产者不停地从标准输入读取数据,而消费者从生产者中获取数据并输出到标准输出:

function producer ()
    while true do
        local x = io.read() -- produce new value
        send(x)             -- send to consumer
    end
end
function consumer (prod)
    while true do
        local x = receive(prod) -- receive from producer
        io.write(x, "\n")       -- consume new value
    end
end

现在的问题是,由谁来执行 yield(),谁来执行 resume() 呢?

当一个 coroutine 调用 yield() 时,它并不是进入了一个新的函数,而是把传递给 yield() 的值返回给唤醒它的 coroutine;当调用 resume() 时也并不是进入一个新的函数,而是获得被唤醒的 coroutine 的返回值。因此在这个例子里 consumer 适合作为 resume() 的调用者(因为 consumer 需要获取 producer 的值),而 producer 作为 yield() 的调用者:

function receive (prod)
    local status, value = coroutine.resume(prod)
    return value
end

function send (x)
    coroutine.yield(x)
end

迭代器

迭代器也可以看成是一个生产者-消费者模型:每次迭代器取出(生产)一个元素,交给主程序(消费)。下面是一个打印全排列的程序:

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

function permgen (a, n)
    local n = n or #a

    if n <= 1 then         -- nothing to change?
        printResult(a)
    else
        for i = 1, n do
            -- put i-th element as the last one
            a[n], a[i] = a[i], a[n]
            -- generate all permutations of the other elements
            permgen(a, n - 1)
            -- restore i-th element
            a[n], a[i] = a[i], a[n]
        end
    end
end

permgen({1, 2, 3, 4})

结合上一个例子(生产者作为 yield() 的调用者,消费者作为 resume() 的调用者),把 permgen() 改动一下:

function permgen (a, n)
    local n = n or #a

    if n <= 1 then         -- nothing to change?
        coroutine.yield(a)
    else
        ......
end

消费者作为一个单独的 coroutine,每次获取一个全排列:

function permutations (a)
    local co = coroutine.create(function () permgen(a) end)
    return function ()   -- iterator
        local code, res = coroutine.resume(co)
        return res
    end
end

for p in permutations{"a", "b", "c"} do
    printResult(p)
end

可以看到这里的 permutations() 和上一个例子的 receive() 结构很相似,在 lua 中为这种结构提供了一个函数:coroutine.wrap()。wrap() 函数接收一个函数作为参数并创建一个对应的 coroutine,但是并不返回这个 coroutine,而是返回一个函数 func,每次调用 func 都会返回从 yield() 获得的值。因此 permutations() 可以写成这样:

function permutations (a)
    return coroutine.wrap(function () permgen(a) end)
end

非抢占式的多线程编程

coroutine 看起来就像操作系统中的线程,但是它不是抢占式的(即在某个 coroutine 运行的时候,除非它自己调用了 yield(),否则外部程序不能让它停下来)。非抢占式比起抢占式的好处是比较简单,不用担心在临界区执行时被打断,一个 coroutine 只需保证自己只有在完成临界区操作后才调用 yield() 把控制权交出去就行了(不要在临界区中调用 yield(),这样相当于被打断了)。

例子太长,略过。

线程和状态

lua 不支持抢占式的多线程和共享内存,一个原因是标准 c 语言并没有提供这些标准,另一个原因是,lua 的作者认为多线程对于 lua 来说并不是必需的,抢占式和共享内存带来复杂性,经常会出问题,这些功能应该在更底层提供,而不是在 lua。

线程

每个线程都有自己单独的栈,几乎每个函数都会有的参数 lua_State 中记录了栈的信息。每当创建一个新的 lua_State 时就会创建一个新的线程及其对应的栈,这个线程就是主线程。要在已有的 lua_State 中创建新的线程使用函数

lua_State *lua_newthread (lua_State *L);

例如:

l = luaL_newstate();
l1 = lua_newthread(l);

新创建的线程会作为原来线程的一个 object 被放到栈顶,而函数 lua_newthread() 的返回值则表示新创建线程所独有的栈:

printf("%d\n", lua_gettop(l1));       --> 0
printf("%s\n", luaL_typename(l, -1)); --> thread

主线程(通过 luaL_newstate() 创建的线程)的资源直到调用 lua_close() 才会被回收,但是 lua_newthread() 创建的线程由 gc 管理,它是否有效取决于它是否在创建它的线程的栈中:

lua_pop(l, 1);         /* L1 now is garbage for Lua */
lua_pushstring(l1, "hello");

执行 lua_pop() 之后说明不再使用 l1,这时 l1 已被加入到回收队列中,后续对 l1 的任何操作都有可能引起程序崩溃。

一旦创建了一个新的 thread,我们可以对它进行任何操作,就像在主线程中一样。其实一般的操作也不需要创建一个新的 thread,它的作用主要是实现 coroutine。

当一个 thread 被挂起以后,可以使用函数(5.2.1 改了接口)

int lua_resume (lua_State *L, lua_State *from, int nargs);

让其恢复运行,就像 coroutine 中的 resume() 的作用。lua_resume() 接收 3 个参数,第一个是对应的 lua_State,第二个是唤醒它的 coroutine 对应的 lua_State,最后是传递给 yield() 的参数个数。和 lua_pcall() 类似,首先把函数及其需要的参数压入栈,然后调用函数。不同之处有 3 点:

  1. 没有指定返回值个数,因为返回值并不是通过 lua_resume() 返回的,而是通过原来被挂起的函数返回的;
  2. 没有错误处理,如果出错了调用栈并不会被清空,可以直接查看栈的内容分析错误信息;
  3. 如果函数再次被挂起,lua_resume() 返回一个特殊值 LUA_YIELD 并且在栈中保存挂起时的环境以便后续恢复。要恢复被挂起的函数只需再次调用 lua_resume() 即可。当 coroutine 执行完后 lua_resume() 返回 LUA_OK。

当 thread 被挂起后,它的栈里只能看到要传递给 yield() 的内容,lua_gettop() 可获取这些返回值个数,下一次调用 lua_resume() 时这些内容都会被传递给 yield()。一般情况下我们使用一个函数作为 coroutine 的主体,在这个函数中可能调用了其它函数,其它函数中如果调用了 yield() 就会退出 lua_resume()。

#include <stdio.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int main(void)
{
    int i, nr_args;
    lua_State *l, *l1;

    l = luaL_newstate();
    luaL_openlibs(l);
    l1 = lua_newthread(l);

    if (luaL_loadfile(l1, "./foo.lua") != 0) {
        fprintf(stderr, "luaL_loadfile err: %s.\n", lua_tostring(l1, -1));
        goto end;
    }

    if (lua_resume(l1, NULL, 0) == LUA_OK)
        printf("script init ok.\n");

    lua_getglobal(l1, "foo1");
    lua_pushinteger(l1, 20);

    if (lua_resume(l1, NULL, 1) == LUA_YIELD) {
        printf("coroutine yields.\n");
        nr_args = lua_gettop(l1);
        printf("number of args: %d\n", nr_args); /* --> 2 */
        for (i = 1; i <= nr_args; ++i)
            printf("arg[%d]: %d\n", i, lua_tointeger(l1, i)); /* --> 10 21 */
    }

    if (lua_resume(l1, NULL, 0) == LUA_OK) {
        printf("coroutine finishes.\n");
        nr_args = lua_gettop(l1);
        printf("number of args: %d\n", nr_args); /* --> 1 */
        for (i = 1; i <= nr_args; ++i)
            printf("arg[%d]: %d\n", i, lua_tointeger(l1, i)); /* --> 3 */
    }

end:
    lua_close(l);
    return 0;
}

其中使用到的脚本为

function foo (x)
    coroutine.yield(10, x)
end

function foo1 (x)
    foo(x + 1)
    return 3
end

脚本中第一次 yield() 的时候传递给 yield() 的参数是 10 和 21;再次执行 lua_resume() 后 coroutine 函数已经退出,因此栈中的结果是函数的最后返回值。

由于脚本中的函数也可能调用 c 函数,因此 lua 提供了函数

int lua_yield (lua_State *L, int nresults);

实现挂起功能。因为在 c 中一个函数不能自己挂起自己,因此这个函数主要是为了挂起调用者,并且调用者应该是一个 lua 函数。一般来说在 c 函数中的调用形式是这样的:

return lua_yield(L, nres);

第二个参数表示要返回给 resume() 调用者的参数个数。当线程调用 resume() 的时候,这些参数就会被传递给调用者。

因为 c 中不能在循环中调用 yield()(这是因为 resume 后不是回到原来的循环继续执行而是已经跑到函数外了?),因此可以在 lua 中使用循环,而在 c 函数中执行一次操作。例如一个读取数据的函数,如果没有数据就挂起,有数据就返回,c 语言中的函数可以这样写:

int prim_read (lua_State *L) {
    if (nothing_to_read())
        return lua_yield(L, 0);
    lua_pushstring(L, read_some_data());
    return 1;
}

即如果读到数据就返回,没有数据就挂起调用者。对应的 lua 脚本如下:

function read ()
    local line
    repeat
        line = prim_read()
    until line
    return line
end

当 prim_read() 没有读到数据时,lua 脚本被挂起,停在给 line 赋值的地方;当 resume() 时,因为没有返回值,因此重新进入 repeat 循环,再次调用 prim_read(),直到读到数据退出 repeat 循环,然后 read() 返回读到的结果。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注