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.lualocal 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.lualocal 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")
endlocal 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_cprintf("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");elseluaG_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.

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/21538.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/21538.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/21538.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【最新鴻蒙應用開發】——實用廣告思路,可動態修改(方便運營)

鴻蒙項目加入廣告展示頁業務 廣告頁的思路——華為有廣告業務&#xff0c;但是我們不用- ad模塊&#xff1b; 想自定義廣告——場景&#xff1a; app啟動-有廣告需求&#xff0c;就打開廣告頁&#xff0c;沒有的話就去登錄或者主頁&#xff1b; 騰訊體育的廣告- 啟動有廣告頁…

適合小白學習的項目1894java開發ssm框架校園跑腿管理系統myeclipse開發mysql數據庫springMVC模式java編程計算機網頁設計

一、源碼特點 java ssm 校園跑腿管理系統是一套完善的web設計系統&#xff08;系統采用SSM框架進行設計開發&#xff0c;springspringMVCmybatis&#xff09;&#xff0c;對理解JSP java編程開發語言有幫助&#xff0c;系統具有完整的源代碼和數據庫&#xff0c;系統主要采…

Java項目:96 springboot精品在線試題庫系統

作者主頁&#xff1a;舒克日記 簡介&#xff1a;Java領域優質創作者、Java項目、學習資料、技術互助 文中獲取源碼 項目介紹 這次開發的精品在線試題庫系統有管理員&#xff0c;教師&#xff0c;學生三個角色。 管理員功能有個人中心&#xff0c;專業管理&#xff0c;學生管理…

比較(二)利用python繪制雷達圖

比較&#xff08;二&#xff09;利用python繪制雷達圖 雷達圖&#xff08;Radar Chart&#xff09;簡介 雷達圖可以用來比較多個定量變量&#xff0c;也可以用于查看數據集中變量的得分高低&#xff0c;是顯示性能表現的理想之選。缺點是變量過多容易造成閱讀困難。 快速繪制…

Go語言 一些問題了解

一、讀取文件數據&#xff0c;是阻塞還是非阻塞的&#xff1f; 分兩種情況&#xff1a;常規讀取文件數據&#xff0c;和網絡IO讀取數據 1. 常規讀取文件數據&#xff1a; io.Reader 和 bufio.Reader 是同步進行的。 bufio.Reader 提供緩沖的讀取操作&#xff0c;意味著數據是…

網站入門:Flask用法講解

Flask是一個使用Python編寫的輕量級Web服務框架&#xff0c;旨在幫助開發人員快速構建和部署Web應用程序。下面將對Flask進行更為詳細的解釋說明&#xff0c;并展示其使用示例與注意事項&#xff1a; 1.解釋說明 定義及特點: Flask以其簡潔和靈活著稱&#xff0c;允許開發者以…

C++:list模擬實現

hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起學習《C&#xff1a;list模擬實現》&#xff0c;感謝大家對我上一篇的支持&#xff0c;如有什么問題&#xff0c;還請多多指教 &#xff01; 如果本篇文章對你有幫助&#xff0c;還請各位點點贊&#xff01;&#xf…

LeetCode題練習與總結:二叉樹展開為鏈表--114

一、題目描述 給你二叉樹的根結點 root &#xff0c;請你將它展開為一個單鏈表&#xff1a; 展開后的單鏈表應該同樣使用 TreeNode &#xff0c;其中 right 子指針指向鏈表中下一個結點&#xff0c;而左子指針始終為 null 。展開后的單鏈表應該與二叉樹 先序遍歷 順序相同。 …

深入探討Java字符串拼接的藝術

引言 在Java編程中&#xff0c;字符串是最基本的數據類型之一。字符串拼接是開發過程中一個非常常見的操作&#xff0c;無論是構建用戶界面的文本&#xff0c;還是生成日志信息&#xff0c;都離不開字符串的拼接。然而&#xff0c;字符串拼接的效率和正確性常常被開發者忽視&a…

格式化數據恢復指南:從備份到實戰,3個技巧一網打盡

朋友們&#xff01;你們有沒有遇到過那種“啊&#xff0c;我的文件呢&#xff1f;”的尷尬時刻&#xff1f;無論是因為手滑、電腦抽風還是其他原因&#xff0c;數據丟失都可能會讓我們抓狂&#xff0c;甚至有時候&#xff0c;我們可能一不小心就把存儲設備格式化了&#xff0c;…

香橙派OrangePI AiPro測評 【運行qt,編解碼,xfreeRDP】

實物 為AI而生 打開盒子 配置 扛把子的 作為業界首款基于昇騰深度研發的AI開發板&#xff0c;Orange Pi AIpro無論在外觀上、性能上還是技術服務支持上都非常優秀。采用昇騰AI技術路線&#xff0c;集成圖形處理器&#xff0c;擁有8GB/16GB LPDDR4X&#xff0c;可以外接32…

進程通信——管道

什么是進程通信&#xff1f; 進程通信是實現進程間傳遞數據信息的機制。要實現數據信息傳遞就要進程間共享資源——內存空間。那么是哪塊內存空間呢&#xff1f;進程間是相互獨立的&#xff0c;一個進程不可能訪問其他進程的內存空間&#xff0c;那么這塊空間只能由操作系統提…

什么是RPA自動化辦公?

RPA自動化辦公&#xff1a;提升效率的利器 如今&#xff0c;自動化辦公已成為提升效率、減少錯誤、節省成本的關鍵手段。RPA&#xff08;機器人流程自動化&#xff0c;Robotic Process Automation&#xff09;作為其中的重要組成部分&#xff0c;正受到越來越多企業的青睞。那…

【全開源】簡單商城系統源碼(PC/UniAPP)

提供PC版本、UniAPP版本(高級授權)、支持多規格商品、優惠券、積分兌換、快遞鳥電子面單、支持移動端樣式、統計報表等 提供全部前后臺無加密源代碼、數據庫離線部署。 構建您的在線商店的基石 一、引言&#xff1a;為什么選擇簡單商城系統源碼&#xff1f; 在數字化時代&am…

【Spring Cloud Alibaba】初識Spring Cloud Alibaba

目錄 回顧主流的微服務框架Spring Cloud 版本簡介Spring Cloud以往的版本發布順序排列如下&#xff1a; 由停更引發的"升級慘案"哪些Netflix組件被移除了&#xff1f; 替換方案服務注冊中心&#xff1a;服務調用&#xff1a;負載均衡&#xff1a;服務降級&#xff1a…

Python—面向對象小解(6)-閉包、裝飾器

一、閉包 在Python中&#xff0c;閉包&#xff08;closure&#xff09;是一個函數對象&#xff0c;即使在其詞法作用域外被調用&#xff0c;它仍然能訪問該作用域內的變量。閉包通過“捕獲”周圍作用域的變量&#xff0c;保持這些變量的狀態&#xff0c;即使在外部函數已經返回…

干貨分享 | TSMaster 中 Hex 文件編輯器使用詳細教程

TSMaster 軟件的 Hex 文件編輯器提供了文件處理的功能&#xff0c;這一特性讓使用 TSMaster 軟件的用戶可以更便捷地對 Hex、bin、mot、s19 和 tsbinary 類型的文件進行處理。 本文重點講述 TSMaster 中 Hex 文件編輯器的使用方法&#xff0c;該編輯器能實現將現有的 Hex、bin、…

@vue-office/excel 解決移動端預覽excel文件觸發軟鍵盤

先直接上代碼 不耽誤大家時間 標明下插件庫 非常感謝作者提供預覽插件 vue-office/excel 只需要控制CSS :deep(.x-spreadsheet-overlayer) {.x-spreadsheet-selectors {display: none !important;} } :deep(.x-spreadsheet-bottombar) {li.active {user-select: none !import…

家政上門系統源碼,家政上門預約服務系統開發涉及的主要功能

家政上門預約服務系統開發是指建立一個在線平臺或應用程序&#xff0c;用于提供家政服務的預約和管理功能。該系統的目標是讓用戶能夠方便地預約各種家政服務&#xff0c;如保潔、家庭護理、月嫂、家電維修等&#xff0c;并實現服務供應商管理和訂單管理等功能。 以下是開發家政…

Windows API 速查

Windows API 函數大全 (推薦)&#xff1a;https://blog.csdn.net/xiao_yi_xiao/article/details/121604742Windows API 在線參考手冊&#xff1a;http://www.office-cn.net/t/api/index.html?web.htmWindows 開發文檔 (官方)&#xff1a;https://learn.microsoft.com/zh-cn/wi…