lua vm 一: attempt to yield across a C-call boundary 的原因分析

使用 lua 的时候有时候会遇到这样的报错:“attempt to yield across a C-call boundary”。


1. 网络上的解释

可以在网上找到一些关于这个问题的解释。


1.1 解释一

这个 issue:一个关于 yield across a C-call boundary 的问题,云风的解释是:

C (skynet framework)->lua (skynet service) -> C -> lua
最后这个 lua 里如果调用了 yield 就会产生。


云风这样解释没问题,但太简了,只说了这样会导致报错,但没具体说为什么会报错。


1.2 解释二

这篇文章 lua中并不能随意yield 提到:

流程:coroutine --> c --> coroutine --> yield ===> 报错
为什么这种情况下lua会给出这种报错呢?主要是因为在从c函数调回到coroutine中yield时,coroutine当前的堆栈情况会被保存在lua_State中,因此在调用resume时,lua可以恢复yield时的场景,并继续执行下去。但c函数不会因为coroutine的yield被挂起,它会继续执行下去,函数执行完后堆栈就被销毁了,所以无法再次恢复现场。而且因为c函数不会被yield函数挂起,导致c和lua的行为也不一致了,一个被挂起,一个继续执行完,代码逻辑很可能因此出错。


这个接近于胡说了。


1.3 解释三

这篇文章 深入Lua:在C代码中处理协程Yield 提到:

原因是Lua使用longjmp来实现协程的挂起,longjmp会跳到其他地方去执行,使得后面的C代码被中断。l_foreach函数执行到lua_call,由于longjmp会使得后面的指令没机会再执行,就像这个函数突然消失了一样,这肯定会引起不可预知的后果,所以Lua不允许这种情况发生,它在调用coroutine.yield时抛出上面的错误。


作者点出了问题的关键: “由于longjmp会使得后面的指令没机会再执行”,但讲得不够细,对于问题产生的条件没有讲清楚。


1.4 小结

以上解释,感觉都没有把这个问题说清楚,需要深入到 lua vm 的工作机制才能解释清楚,所以有了这篇文章。


2. 从原理上分析问题

问题的关键就在于:

  • lua 是通过 setjmp/longjmp 实现 resume/yield 的。

  • lua 函数只操作 lua 数据栈,而 c 函数不止操作 lua 数据栈,还会操作 c 栈(即操作系统线程的栈)。

  • 每个 lua 协程都有一个独立的 lua 数据栈,但每个系统线程只有一个公共的 c 栈。

  • 在协程的函数调用链中,会有 lua 函数也会有 c 函数,如果调用链中有 c 函数,并且在更后续的调用中出现 yield,就会 longjmp 回到 resume (setjmp) 之处,从而导致 c 函数依赖的 c 栈被恢复执行的协程的 c 函数调用给覆盖掉。

setjmp/longjmp 示意图:

c 栈从栈底向栈顶生长 

      栈底
    |     |
    |     |
    |-----| co1 resume (setjmp) <-
    |     |                      | 
    | co2 |                      |
    |stack|                      |
    |-----| co2 yield (longjmp) ->
      栈顶

不懂 setjmp / longjmp 怎么工作的,可以参考这篇文章,讲得很细了: setjmp是怎么工作的 。


图1:lua yield 示意图

上图中:

1、co1 resume 了 co2,co2 开始执行,co2 的 callinfo 调用链中有 lua 也有 c 函数,其中的 c 函数会操作 lua 数据栈和 c 栈,c 栈在图中就是 “co2 c stack” 那一块内存。

2、co2 yield 的时候,co2 停止执行,co1 从上次 resume 处恢复。

3、co1 继续往下执行,必然会有 c 函数调用,co1 的 c 函数会把 “co2 c stack” 这块内存覆盖掉,这意味着 co2 那些还没执行完成的 c 函数的 c 栈被破坏了,即使 co2 再次被 resume,也无法正常运行了。


3. 从代码上分析问题

其实讲完原理就够了,但是 lua 在 yield 这个问题上会选择性不报错,所以还是有必要从源码上讲一讲。

以下分析使用的 lua 版本是 5.3.6(lua-5.2 跟 lua-5.4 也是差不多的)。

lua-5.3.6 官方下载链接: https://lua.org/ftp/lua-5.3.6.tar.gz 。

笔者的 github 也有 lua-5.3.6 源码的 copy: https://github.com/antsmallant/antsmallant_blog_demo/tree/main/3rd/lua-5.3.6 。


下文展示的 demo 代码都在此(有 makefile,可以直接跑起来):https://github.com/antsmallant/antsmallant_blog_demo/tree/main/blog_demo/lua-co-yield 。


3.1 情况一:lua 调用 c,在 c 中直接 yield

结果
yield 时不会报错,但实际上也没能正常工作。

不报错的原因
这是 lua 官方的设定,lua 调用 c 函数或者其他什么函数,都是被编译成 OP_CALL 指令,而 OP_CALL 并不会设一个标志位导致后面有 yield 的时候报错;而 c 调用 lua 是用 lua_call 这个 api,它会设置一个标志位,后面 yield 时判断到标志位就报错: “attempt to yield across a C-call boundary”。

没能正常工作的原因
上面分析过了,yield 之后,协程的 c 栈被恢复执行的协程覆盖掉了。


上代码吧。


lua 代码:test_co_1.lua

-- test_co_1.lua

local co = require "coroutine"
local clib = require "clib"

local co2 = co.create(function()
    clib.f1()
end)

-- 第一次 resume
local ok1, ret1 = co.resume(co2)
print("in lua:", ok1, ret1)

-- 第二次 resume
local ok2, ret2 = co.resume(co2)
print("in lua:", ok2, ret2)

c代码:clib.c

// clib.c

#include <stdlib.h>
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>

static int f1(lua_State* L) {
    printf("clib.f1: before yield\n");

    lua_pushstring(L, "yield from clib.f1");
    lua_yield(L, 1);
    
    printf("clib.f1: after yield\n");

    return 0;
}

LUAMOD_API int luaopen_clib(lua_State* L) {
    luaL_Reg funcs[] = {
        {"f1", f1},
        {NULL, NULL}
    };
    luaL_newlib(L, funcs);
    return 1;
}

输出:

clib.f1: before yield
first time return:      true    yield from clib.f1
second time return:     true    nil

clib.f1 的这句代码 printf("clib.f1: after yield\n"); 在第二次 resume 的时候没有被执行,但代码也没报错,跟开头说的结果一样。lua 大概是认为没有人会这样写代码吧。

这种情况,如果要让 clib.f1 能执行 yield 之后的,需要把 lua_yield 换成 lua_yieldk,然后把 yield 之后要执行的逻辑放到另一个函数里,类似这样:



int f2_after_yield(lua_State* L, int status, lua_KContext ctx) {
    printf("clib.f2: after yield\n");
    return 0;
}

static int f2(lua_State* L) {
    printf("clib.f2: before yield\n");

    lua_pushstring(L, "yield from clib.f2");
    lua_yieldk(L, 1, 0, f2_after_yield);
    
    return 0;
}


3.2 情况二:c 调用 lua,lua 后续调用出现 yield

结果
yield 时会报错 “attempt to yield across a C-call boundary”。

原因
上面原理的时候分析过了,源码实现上,c 调用 lua 是用的 lua_call 这个 api,它会设置一个标志位,在后续调用链中(无论隔了多少层,无论是 c 还是 lua)只要执行了 yield,都会判断标志位,然后触发报错。


上代码吧。


lua 代码: test_co_3.lua

-- test_co_3.lua

local co = require "coroutine"
local clib = require "clib"

function lua_func_for_c()
    print("enter lua_func_for_c")
    co.yield()
    print("leave lua_func_for_c")
end

local co2 = co.create(function()
    print("enter co2")
    clib.f3()
    print("leave co2")
end)

local ok, err = co.resume(co2)
print(ok, err)


c 代码:clib.c


// clib.c

#include <stdlib.h>
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>

static int f3(lua_State* L) {
    printf("enter f3\n");
    lua_getglobal(L, "lua_func_for_c");  
    lua_call(L, 0, 0); // 调用 lua 脚本里定义的 lua 函数: lua_func_for_c
    printf("leave f3\n");
    return 0;
}

LUAMOD_API int luaopen_clib(lua_State* L) {
    luaL_Reg funcs[] = {
        {"f3", f3},
        {NULL, NULL}
    };
    luaL_newlib(L, funcs);
    return 1;
}


输出:

enter co2
enter f3
enter lua_func_for_c
false   attempt to yield across a C-call boundary

clib 里的 c 函数 f3,通过 lua_call 调用 lua 脚本里面定义的 lua 函数 lua_func_for_c,而 lua_func_for_c 里面会 yield,所以这种情况下 yield 就直接报错了。


3.3 lua_call 是如何阻止后续 yield 的?

直接看 lua 源码,lua_callk 会调用到 luaD_callnoyield,而 luaD_callnoyield 设置了标志位:

L->nny++;

而在 lua_yieldk 中,判断了标志位:

  if (L->nny > 0) {
    if (L != G(L)->mainthread)
      luaG_runerror(L, "attempt to yield across a C-call boundary");
    else
      luaG_runerror(L, "attempt to yield from outside a coroutine");
  }

4. 问题总结 & 解决办法


4.1 问题总结

经过上面分析,可以看到,问题的核心在于 lua 的多个协程共用一个 c 栈,而协程里面 c 函数调用又会依赖 c 栈,如果在它返回之前就 yield 了,则它依赖的 c 栈会被其他协程覆盖掉,也就无法恢复运行了。按照 luajit 的说法,lua 官方实现不是一种 “fully resumable vm”。

这里面 yield 又分两种情况:

情况症状原因
lua调c不报错,但也不正常工作lua 里调用函数(无论 lua 或 c),都是编译成 OP_CALL 指令,这个指令的实现不会设置让 yield 报错的标志位
c调lua报错用的是 lua_call,它会设置让 yield 报错的标志位

4.2 解决办法


4.2.1 lua-5.2 及以上

lua 对于此问题的解决方案是引入 lua_callk / lua_pcallk / lua_yieldk,要求使用者把 yield 之后要执行的东西放到一个单独的函数 (类型为 lua_KFunction) 里,k 意为 continue,把这个 k 函数作为参数传给 lua_callk / lua_pcallk / lua_yieldk,这个 k 函数会被记录起来,等 yield 返回的时候调用它。

显然,lua 官方的这种方案有点操蛋,但也不失为一种办法。

不过悲催的是,lua 5.2 才引入 kfunction 的,所以 lua-5.1 要用其他的办法。


4.2.2 lua-5.1

lua-5.1 有两个办法,都与 luajit 相关。


方法一:使用 luajit

直接使用 luajit ( https://luajit.org/luajit.html ),luajit 支持 “Fully Resumable VM”[1]:

The LuaJIT VM is fully resumable. This means you can yield from a coroutine even across contexts, where this would not possible with the standard Lua 5.1 VM: e.g. you can yield across pcall() and xpcall(), across iterators and across metamethods.


方法二:使用 lua-5.1.5 + coco 库

coco 库是 luajit 下面的一个子项目 ( https://coco.luajit.org/index.html ),它可以独立于 luajit 之外使用的,但它只能用于 lua-5.1.5 版本。

它的介绍[2]:

Coco is a small extension to get True C Coroutine semantics for Lua 5.1. Coco is available as a patch set against the standard Lua 5.1.5 source distribution.

Coco is also integrated into LuaJIT 1.x to allow yielding for JIT compiled functions. But note that Coco does not depend on LuaJIT and works fine with plain Lua.

coco 库能做到从 c 调用中恢复,是因为它为每个协程准备了专用的 c 栈:“Coco allows you to use a dedicated C stack for each coroutine”[2]。

所以,如果不使用 luajit,就使用官方的 lua-5.1.5,再 patch 上这个 coco 库就可以了。


5. 总结

  • lua 官方实现的 vm 不是 “fully Resumable” 的,原因在于多个协程共用 c 栈,会导致协程的函数调用链中有 c 函数的情况下,yield 报错或工作不正常。

  • lua 提供的函数中,有些使用了 lua_call/lua_pcall,容易导致 yield 报错,比如 lua 函数:require,c 函数:luaL_dostring、luaL_dofile。

  • lua 提供的函数中,有些使用了 lua_callk/lua_pcallk 规避 yield 报错,比如 lua 函数:dofile。

  • lua-5.2 及以上的,可以使用 lua_callk / lua_pcallk / lua_yieldk 来规避 yield 报错问题。

  • lua-5.1 可以使用 luajit 或 lua-5.1.5官方版本+coco库的方法来解决 yield 报错问题。


6. 参考

[1] luajit. extensions. Available at https://luajit.org/extensions.html.

[2] luajit. Coco — True C Coroutines for Lua. Available at https://coco.luajit.org/index.html.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/679184.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ctfshow解题,知识点学习

1.easy_zip&#xff08;misc&#xff09; 1&#xff09;打开环境后是一个压缩包&#xff0c;解压里面有个flag.txt文件需要密码&#xff0c; 2&#xff09;直接用工具爆破&#xff0c;即可找到密码 2.easy_eval 1&#xff09;进入题目环境&#xff0c;先进行代码审计 首先说是…

【前端技术】 ES6 介绍及常用语法说明

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

个人网站建设方案书

个人网站建设方案书 一、项目背景 随着互联网的迅猛发展&#xff0c;个人网站已经成为展示个人能力、情感表达的重要平台。无论是个人品牌推广&#xff0c;还是个人作品展示&#xff0c;个人网站都能够为个人提供一个独特的展示空间。因此&#xff0c;建设一个个人网站已经成为…

【AIGC】PULID:对比对齐的ID定制化技术

论文链接&#xff1a;https://arxiv.org/pdf/2404.16022 github&#xff1a;https://github.com/ToTheBeginning/PuLID comfyui节点&#xff1a;GitHub - cubiq/PuLID_ComfyUI: PuLID native implementation for ComfyUI 论文亮点 增加了对比对齐loss和ID loss,最大限度减少…

MoE 大模型的前世今生

节前&#xff0c;我们星球组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、参加社招和校招面试的同学。 针对算法岗技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备、面试常考点分享等热门话题进行了深入的讨论。 合集&#x…

【TB作品】MSP430G2553单片机读取DS18B20温度传感器,OLED显示

功能 MSP430G2553单片机读取DS18B20 0.96寸 IIC 四针OLED 部分程序 uint temp_value 0; /* 温度 */ int main(void) {unsigned char xianshi[10];WDTCTL WDTPW WDTHOLD; /* Stop WDT */OLED_Init(); /* OLED初始化 *///显示汉字 温度&#xff1a;摄氏度OLED_ShowCHinese(…

Socket编程学习笔记之TCP与UDP

Socket&#xff1a; Socket是什么呢&#xff1f; 是一套用于不同主机间通讯的API&#xff0c;是应用层与TCP/IP协议族通信的中间软件抽象层。 是一组接口。在设计模式中&#xff0c;Socket其实就是一个门面模式&#xff0c;它把复杂的TCP/IP协议族隐藏在Socket接口后面&#…

npm install 出错,‘proxy‘ config is set properly. See: ‘npm help config‘

背景 从远程clone下项目之后&#xff0c;使用命令 npm install 安装依赖&#xff0c;报错如下 意为&#xff1a; 报错&#xff1a; npm犯错!network与网络连通性有关的问题。 npm犯错!网络在大多数情况下&#xff0c;你背后的代理或有坏的网络设置。 npm犯错!网络 npm犯错…

C++基础与深度解析 | 类进阶 | 运算符重载 | 类的继承 | 虚函数

文章目录 一、运算符重载二、类的继承1.类的继承2.虚函数 一、运算符重载 在C中&#xff0c;operator关键字用于重载运算符&#xff0c;使得类的实例可以使用内置的操作符&#xff08;如、-、*、/等&#xff09;进行操作。 运算符重载的特性&#xff1a; 重载不能发明新的运算…

Fast-Retry:一个支持百万级多任务异步重试框架【送源码】

前言 假设你的系统里有100万个用户&#xff0c;然后你要轮询重试的获取每个用户的身份信息, 如果你还在使用SpringRetry和GuavaRetry 之类的这种单任务的同步重试框架&#xff0c;那你可能到猴年马月也处理不完&#xff0c;即使加再多的机器和线程也是杯水车薪&#xff0c;而F…

6.4 cf E(题目难理解)

Problem - E - Codeforces 翻译&#xff1a; 小车在0点&#xff0c;时间为0时开始移动&#xff0c;从0&#xff0c;a1,a2......ak有k1个标志点&#xff0c;对应的时间为0&#xff0c;b1,b2...bk 在任意两个标志间&#xff0c;小车以匀速行驶&#xff0c;所以 vai1​−ai​​…

SpringBoot 统一返回格式

目录 一、为什么要统一返回&#xff1f; 二、全局异常处理代码 三、统一返回对象代码 四、使用方法 五、结果展示 一、为什么要统一返回&#xff1f; 在Spring Boot应用中&#xff0c;为了保持API接口的响应格式统一&#xff0c;通常会采用全局异常处理和自定义返回对象的方…

Sd-CDA (自退化对比域适应框架):解决工业故障诊断中数据不平衡问题

现代工业故障诊断任务常常面临分布差异和双不平衡的双重挑战。现有的域适应方法很少关注普遍存在的双不平衡问题&#xff0c;导致域适应性能差或甚至产生负面迁移。在这项工作中&#xff0c;提出了一种自降级对比域适应&#xff08;SdCDA&#xff09;诊断框架&#xff0c;用于处…

如何实现单例模式及不同实现方法分析-设计模式

这是 一道面试常考题&#xff1a;&#xff08;经常会在面试中让手写一下&#xff09; 什么是单例模式 【问什么是单例模式时&#xff0c;不要答非所问&#xff0c;给出单例模式有两种类型之类的回答&#xff0c;要围绕单例模式的定义去展开。】 单例模式是指在内存中只会创建…

Nginx location 与 Rewrite

Nginx正则表达式 location 通过前缀或正则匹配用户的URL访问路径做页面跳转、访问控制和代理转发 location 大致可以分为三类&#xff1a; 精准匹配&#xff1a;location / {...} 一般匹配&#xff1a;location / {...} 正则匹配&#xff1a;location ~ / {...} location…

外汇天眼:Bitpanda 扩大与德意志银行的合作

金融科技独角兽Bitpanda正在扩大与德意志银行的合作&#xff0c;为德国用户提供实时支付解决方案&#xff0c;以处理进出交易。 这种基于API的账户解决方案将使Bitpanda能够访问德国的IBAN账户&#xff0c;优化和增强用户体验&#xff0c;同时确保信任、速度和效率。 这只是Bi…

通过仪器分类方式修订看监测仪器发展新趋势

随着科技的进步和监测需求的不断升级&#xff0c;监测仪器的分类方式亟需与时俱进。本文旨在探讨《混凝土坝监测仪器系列型谱》中对现有仪器分类方式的修订&#xff0c;以及监测仪器发展的新趋势相关内容。 一、仪器分类方式的修订 传统的仪器分类方式往往基于功能、原理或应用…

太极图形课——渲染——光线追踪实战第一部分呢

根据概念部分我们逐步通过太极实现光线追踪 总共可以分为5步 第一步&#xff1a;如何发射出一道光&#xff1f; 首先明确何为一道光&#xff0c;光从我们眼睛&#xff08;摄像机&#xff09;射出&#xff0c;那么在三维虚拟世界里&#xff0c;我们可以认为这道光就是一条射线…

【微信小程序】事件绑定和事件对象

文章目录 1.什么是事件绑定2.button组件3.事件绑定4.input组件 1.什么是事件绑定 小程序中绑定事件与在网页开发中绑定事件几乎一致&#xff0c;只不过在小程序不能通过on的方式绑定事件&#xff0c;也没有click等事件&#xff0c;小程序中 绑定事件使用bind方法&#xff0c;c…

6个音效、配乐素材网站,免费可商用

视频剪辑必备的6个音效、配乐素材网站&#xff0c;免费下载&#xff0c;剪辑师们赶紧收藏&#xff01; 1、菜鸟图库 音效素材下载_mp3音效大全 - 菜鸟图库 菜鸟图库音效素材免费下载。站内不仅有大量音频素材&#xff0c;还有很多设计、办公、图片、视频等素材。音频素材全部都…