Lua 5.3 源码分析(十)线程的执行与中断

Lua 5.3 源码分析(十)线程的执行与中断

Lua 的程序运行时以线程为单位的。每个Lua 线程可以独立运行直到自行中断,把中断的信息留在状态机中。每条线程的执行互不干扰,可以独立延续之前中断的执行过程。

Lua 线程和系统线程无关,所以不会为每条 Lua 线程创建独立的系统堆栈,而是利用自己维护的线程栈,内存开销也就远小于系统线程。

Lua 是一门嵌入式语言,和 C 语言混合编程是一种常态。 一旦Lua 调用的 C 库中企图中断线程,延续它就是一个巨大的难题。

异常处理

如果Lua 被实现为一个纯粹的运行在字节码 VM 上的语言,只要不出 VM,可以很容易的实现自己的线程和异常处理。

事实上,Lua 的函数调用层次上只要没有 C 函数,是不会在 C 层面的调用栈上深入下去的。

但当Lua 函数调用了 C 函数,而这个 C 函数又进一步回调了 Lua 函数,这个问题就复杂很多。

Lua 的标准库中的 pairs 函数,就是一个典型的 C 扩展函数,却又回调了 Lua 函数。

Lua 底层把异常和线程中断用同一种机制来处理,也就是使用了 C 语言标准的 longjmp 机制来解决这个问题。
#if !defined(LUAI_THROW) /* { */

#if defined(__cplusplus) && !defined(LUA_USE_LONGJMP)   /* { */

/* C++ exceptions */
#define LUAI_THROW(L,c)     throw(c)
#define LUAI_TRY(L,c,a) \
try { a } catch(...) { if ((c)->status == 0) (c)->status = -1; }
#define luai_jmpbuf     int  /* dummy variable */

#elif defined(LUA_USE_POSIX)                /* }{ */

/* in POSIX, try _longjmp/_setjmp (more efficient) */
#define LUAI_THROW(L,c)     _longjmp((c)->b, 1)
#define LUAI_TRY(L,c,a)     if (_setjmp((c)->b) == 0) { a }
#define luai_jmpbuf     jmp_buf

#else                           /* }{ */

/* ISO C handling with long jumps */
#define LUAI_THROW(L,c)     longjmp((c)->b, 1)
#define LUAI_TRY(L,c,a)     if (setjmp((c)->b) == 0) { a }
#define luai_jmpbuf     jmp_buf

#endif                          /* } */

#endif                          /* } */

每条线程 L 中保存了当前的 longjmp 返回点: errorJmp ,其结构定义 为 struct lua_longjmp 。这是一条链表,每次运行一段受保护的 Lua 代码,都会生成一个新的错误返回点,链到这条链表上。

/* chain list of long jump buffers */
struct lua_longjmp {
    struct lua_longjmp *previous;
    luai_jmpbuf b;
    volatile int status;  /* error code */
};

设置 longjmp 返回点是由 luaD_rawrunprotected 完成的。

int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) {
    unsigned short oldnCcalls = L->nCcalls;
    struct lua_longjmp lj;
    lj.status = LUA_OK;
    lj.previous = L->errorJmp;  /* chain new error handler */
    L->errorJmp = &lj;
    LUAI_TRY(L, &lj,
             (*f)(L, ud);
             );
    L->errorJmp = lj.previous;  /* restore old error handler */
    L->nCcalls = oldnCcalls;
    return lj.status;
}

这段代码很容易理解:设置新的 jmpbuf,串到链表上,调用函数。调用完成后恢复进入时状态。
如果想回直接返回到最近的错误恢复点,只需要调用 longjmp。Lua 使用一个内部API luaD_throw 封装了这个过程。

l_noret luaD_throw (lua_State *L, int errcode) {
    if (L->errorJmp) {  /* thread has an error handler? */
        L->errorJmp->status = errcode;  /* set status */
        LUAI_THROW(L, L->errorJmp);  /* jump to it */
    }
    else {  /* thread has no error handler */
        global_State *g = G(L);
        L->status = cast_byte(errcode);  /* mark it as dead */
        if (g->mainthread->errorJmp) {  /* main thread has a handler? */
            setobjs2s(L, g->mainthread->top++, L->top - 1);  /* copy error obj. */
            luaD_throw(g->mainthread, errcode);  /* re-throw in main thread */
        }
        else {  /* no handler at all; abort */
            if (g->panic) {  /* panic function? */
                seterrorobj(L, errcode, L->top);  /* assume EXTRA_STACK */
                if (L->ci->top < L->top)
                    L->ci->top = L->top;  /* pushing msg. can break this invariant */
                lua_unlock(L);
                g->panic(L);  /* call panic function (last chance to jump out) */
            }
            abort();
        }
    }
}

考虑到新构造的线程可能在不受保护的情况下运行。这时的任何错误都必须被捕获,不能让程序崩溃。这种情况合理的处理方式就是把正在运行的线程标记为死线程,并且在主线程中抛出异常。

函数调用

函数调用分为受保护调用和不受保护的调用。
受保护的函数调用可以看到一个 C 层面意义上完整的过程。在Lua 代码中,pcall 是用函数而不是语音机制完成的。受保护的函数调用一定在 C 层面进出一次调用栈。
它使用一个独立的内部API luaD_pcall 来实现。公开 API luaD_pcallk 仅仅是对它做了一些封装。

int luaD_pcall (lua_State *L, Pfunc func, void *u,
                ptrdiff_t old_top, ptrdiff_t ef) {
    int status;
    CallInfo *old_ci = L->ci;
    lu_byte old_allowhooks = L->allowhook;
    unsigned short old_nny = L->nny;
    ptrdiff_t old_errfunc = L->errfunc;
    L->errfunc = ef;
    status = luaD_rawrunprotected(L, func, u);
    if (status != LUA_OK) {  /* an error occurred? */
        StkId oldtop = restorestack(L, old_top);
        luaF_close(L, oldtop);  /* close possible pending closures */
        seterrorobj(L, status, oldtop);
        L->ci = old_ci;
        L->allowhook = old_allowhooks;
        L->nny = old_nny;
        luaD_shrinkstack(L);
    }
    L->errfunc = old_errfunc;
    return status;
}

从这段代码我们可以看到 pcall 的处理模式:用 C 层面的堆栈来保护和恢复状态。
L->ci、L->allowhook、L->nny( nny 的全称是 number of
non-yieldable calls。由于 C 语言本身无法提供延续点的支持,所以 Lua 也无法让所有函数都是 yieldable 的。当一级函数处于 non-yieldable 状态时,更深的层次都无法 yieldable。这个变量用于监督这个状态,在错误发生报告。每级 C 调用是否允许 yield 取决于是否有设置 C 延续点,或是 Lua 内核实现时认为这次调用在发生 yield 时无法正确处理。这些都是由 luaD_call 的最后一个参数来制定。)、L->errfunc 都保存在 luaD_pcall 的 C 堆栈上,一旦 luaD_rawrunprotected 就可以正确恢复。

luaD_rawrunprotected 没有正确返回时,需要根据 old_top 找到堆栈上刚才调用的函数,给它做收尾工作(调用luaF_close 涉及 upvalue 的 gc 流程)。

因为 luaD_rawrunprotected 调用的是一个函数对象,而不是数据栈上的索引,这就需要额外的变量来定位了。

这里使用 restorestack 这个宏来定位栈上的地址,是因为数据栈的内存地址是会随着数据栈的大小而变化。保存地址是不可能的,而应该记住一个相对量。 savestack 和 restorestack这两个宏就是做这个工作的。
#define savestack(L,p) ((char )(p) - (char )L->stack)
#define restorestack(L,n) ((TValue )((char )L->stack + (n)))

一般的 Lua 层面的函数调用并不对应一个 C 层面上函数调用行为。对于Lua 函数而言,应该看成是生成新的 CallInfo,修正数据栈,然后把字节码的执行位置调整到被调用的函数开头。而Lua 函数的return 操作则做了 相反的操作,恢复数据栈,弹出 CallInfo ,修改字节码的执行位置,恢复到原有的执行序列上。

理解了这一点就能明白,在底层 API 中,为何分为 luaD_precall 和 luaD_poscall 。

luaD_precall 执行的是函数调用部分的工作,而 l

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值