lua协程实现及field访问的一个细节

intro

由于C不支持协程,C++只有在最近的C++标准(C++20)中才支持协程,如果希望在C++中支持协程通常需要使用第三方库。针对这种问题,可能有些实现在C++中嵌入lua脚本,利用lua的原生协程机制来达到协程效果;或者干脆使用go这种内置包含了协程的语言。

协程

问题

  • 私有堆栈

协程的一个关键特性就是当“异步”,当执行某个流程的时候,在某个位置切出去,之后再切回来。由于一个执行流执行的时候,很多信息是在堆栈中保存的(包括调用关系),切换前后需要有相同的堆栈,所以协程必须有自己的单独堆栈

  • 切换

当每个协程有了自己的私有堆栈之后,剩余的问题就是yield的时候,控制流转到哪里?这个问题其实相对比较简单,以内核的进程切换为例:当切换的时候其实就是跳转到schedule函数。go的运行时应该也是类似,yield就是执行流切换到调度器来选择下一个可以运行的协程。

实现

  • 私有堆栈

为了让实现能够感知到需要创建一个协程,必须通过某种方法声明一个协程。这个在lua中通过coroutine.create;在go中通过go关键字;C++20使用的貌似是无栈协程,这种实现由于没有堆栈,不太容易实现复杂的逻辑,使用也不太友好。

这里关键的意思是:lua/go使用协程的时候都要先声明,这个声明让语言可以为这个协程准备私有堆栈。

static int luaB_cocreate (lua_State *L) {
  lua_State *NL;
  luaL_checktype(L, 1, LUA_TFUNCTION);
  NL = lua_newthread(L);
  lua_pushvalue(L, 1);  /* move function to top */
  lua_xmove(L, NL, 1);  /* move function from L to NL */
  return 1;
}
  • 协程入口

和私有堆栈对应的,有一个单独协程入口函数的概念:用来决定从哪个函数开始,函数的堆栈放在这些私有堆栈上。

  • 切换

lua线程创建之后并不会立即运行,而是需要通过resume开始运行,当执行resume函数时,运行时会通过setjmp设置一个跳转点,coroutine执行到yield的时候直接longjmp到resume即可恢复resume的执行。

也就是说,lua的yield并不是返回到某个调度器,而是返回到了resume位置,因为协程创建/yield之后必须通过resume恢复执行。

对应的跳转结构为


/* 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							/* } */

当resume时,通过LUAI_TRY保存当前位置。

int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) {
  l_uint32 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;
}

当yield时,会通过longjmp跳转到resume(LUAI_THROW)设置的位置。也就是说,协程主要是被resume驱动的,执行流执行resume来调用协程逻辑,协程yield之后返回到resume。


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);
    errcode = luaE_resetthread(L, errcode);  /* close all upvalues */
    if (g->mainthread->errorJmp) {  /* main thread has a handler? */
      setobjs2s(L, g->mainthread->top.p++, L->top.p - 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? */
        lua_unlock(L);
        g->panic(L);  /* call panic function (last chance to jump out) */
      }
      abort();
    }
  }
}

栗子

在lua中,创建一个协程并不会立即执行,需要使用resume启动第一次运行。

lua

tsecer@harry: cat no_resume.lua 
co = coroutine.create(function ()
   print("hello world")
end)

tsecer@harry: lua no_resume.lua
tsecer@harry: 

go

golang中当通过go启动一个协程的时候,协程立即开始执行。

tsecer@harry: cat gofunc.go 
package main

import (
    "fmt"
    "sync"
)

func hi(wg* sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("hello world")
}

func main() {
    var wg sync.WaitGroup

    wg.Add(1)
    go hi(&wg)
    wg.Wait()
}

tsecer@harry: go run gofunc.go      
hello world
tsecer@harry: 

python

python的coroutine也明确说明了创建一些协程之后并不会立即执行:

Note that simply calling a coroutine will not schedule it to be executed:

>>>  
main()  
<coroutine object main at 0x1053bb7c8>  

To actually run a coroutine, asyncio provides the following mechanisms:

  • The asyncio.run() function to run the top-level entry point “main()” function (see the above example.)

  • Awaiting on a coroutine. The following snippet of code will print “hello” after waiting for 1 second, and then print “world” after waiting for another 2 seconds:

在python的源代码中,大致可以看到,await的实现思路和lua大致相同:也是转换为了设置跳转点,然后执行RESUME的模式

static int
compiler_add_yield_from(struct compiler *c, location loc, int await)
{
    NEW_JUMP_TARGET_LABEL(c, send);
    NEW_JUMP_TARGET_LABEL(c, fail);
    NEW_JUMP_TARGET_LABEL(c, exit);

    USE_LABEL(c, send);
    ADDOP_JUMP(c, loc, SEND, exit);
    // Set up a virtual try/except to handle when StopIteration is raised during
    // a close or throw call. The only way YIELD_VALUE raises if they do!
    ADDOP_JUMP(c, loc, SETUP_FINALLY, fail);
    ADDOP_I(c, loc, YIELD_VALUE, 1);
    ADDOP(c, NO_LOCATION, POP_BLOCK);
    ADDOP_I(c, loc, RESUME, await ? RESUME_AFTER_AWAIT : RESUME_AFTER_YIELD_FROM);
    ADDOP_JUMP(c, loc, JUMP_NO_INTERRUPT, send);

    USE_LABEL(c, fail);
    ADDOP(c, loc, CLEANUP_THROW);

    USE_LABEL(c, exit);
    ADDOP(c, loc, END_SEND);
    return SUCCESS;
}

反编译验证

tsecer@harry: cat await_disas.py 
import time
import asyncio
async def foo():
    await asyncio.sleep(1234)

tsecer@harry: ../python -m dis await_disas.py 
  0           RESUME                   0

  1           LOAD_CONST               0 (0)
              LOAD_CONST               1 (None)
              IMPORT_NAME              0 (time)
              STORE_NAME               0 (time)

  2           LOAD_CONST               0 (0)
              LOAD_CONST               1 (None)
              IMPORT_NAME              1 (asyncio)
              STORE_NAME               1 (asyncio)

  3           LOAD_CONST               2 (<code object foo at 0x7f2563275a60, file "await_disas.py", line 3>)
              MAKE_FUNCTION
              STORE_NAME               2 (foo)
              RETURN_CONST             1 (None)

Disassembly of <code object foo at 0x7f2563275a60, file "await_disas.py", line 3>:
   3           RETURN_GENERATOR
               POP_TOP
       L1:     RESUME                   0

   4           LOAD_GLOBAL              0 (asyncio)
               LOAD_ATTR                2 (sleep)
               PUSH_NULL
               LOAD_CONST               1 (1234)
               CALL                     1
               GET_AWAITABLE            0
               LOAD_CONST               0 (None)
       L2:     SEND                     3 (to L5)
       L3:     YIELD_VALUE              1
       L4:     RESUME                   3
               JUMP_BACKWARD_NO_INTERRUPT 5 (to L2)
       L5:     END_SEND
               POP_TOP
               RETURN_CONST             0 (None)
       L6:     CLEANUP_THROW
       L7:     JUMP_BACKWARD_NO_INTERRUPT 5 (to L5)

  --   L8:     CALL_INTRINSIC_1         3 (INTRINSIC_STOPITERATION_ERROR)
               RERAISE                  1
ExceptionTable:
  L1 to L3 -> L8 [0] lasti
  L3 to L4 -> L6 [2]
  L4 to L7 -> L8 [0] lasti
tsecer@harry: 

unix

如果把操作系统的fork看做协程所模拟的原始语义,那么fork也是类似于go的予以,在创建协程/进程的同时启动执行;而反观lua/go两种脚本语言,它们协程的实现都是先创建但是不执行,而是需要使用专门的指令(Resume)等来启动协程运行。

这或许是go和Linux的作者都是Ken Thompson,所以go继承了unix操作系统的实现思想?或者有其它更深层的原因?

field访问

在lua中,obj[xxx]和obj.xxx具有相同的效果,都是访问obj的xxx字段,但是在使用的时候有一个微妙的差别:

通过dot访问的时候field名字是编译时确定的,而[]则会运行时求值。如果一个field是在运行时计算出来的,必须使用[]这种方式。

在解析时,如果是dot访问,只是作为一个字符串来处理,如果是[]格式,则会作为表达式解析。

static void suffixedexp (LexState *ls, expdesc *v) {
  /* suffixedexp ->
       primaryexp { '.' NAME | '[' exp ']' | ':' NAME funcargs | funcargs } */
  FuncState *fs = ls->fs;
  primaryexp(ls, v);
  for (;;) {
    switch (ls->t.token) {
      case '.': {  /* fieldsel */
        fieldsel(ls, v);
        break;
      }
      case '[': {  /* '[' exp ']' */
        expdesc key;
        luaK_exp2anyregup(fs, v);
        yindex(ls, &key);
        luaK_indexed(fs, v, &key);
        break;
      }
      case ':': {  /* ':' NAME funcargs */
        expdesc key;
        luaX_next(ls);
        codename(ls, &key);
        luaK_self(fs, v, &key);
        funcargs(ls, v);
        break;
      }
      case '(': case TK_STRING: case '{': {  /* funcargs */
        luaK_exp2nextreg(fs, v);
        funcargs(ls, v);
        break;
      }
      default: return;
    }
  }
}

两种方式的区别在于是否运行时求值

static void fieldsel (LexState *ls, expdesc *v) {
  /* fieldsel -> ['.' | ':'] NAME */
  FuncState *fs = ls->fs;
  expdesc key;
  luaK_exp2anyregup(fs, v);
  luaX_next(ls);  /* skip the dot or colon */
  codename(ls, &key);
  luaK_indexed(fs, v, &key);
}


static void yindex (LexState *ls, expdesc *v) {
  /* index -> '[' expr ']' */
  luaX_next(ls);  /* skip the '[' */
  expr(ls, v);
  luaK_exp2val(ls->fs, v);
  checknext(ls, ']');
}

outro

lua虽小,五脏俱全,体现了很多大型语言的核心实现方法。

posted on 2024-06-29 18:22  tsecer  阅读(4)  评论(0编辑  收藏  举报

导航