首先,我知道我的问题看起来很熟悉,但实际上我并不是在问为什么在不同的线程之间共享lua状态时会出现seg错误。我实际上是在问,为什么在下面描述的一个具体案例中,它们没有故障。我尽力把它组织得很好,但我意识到它很长。真对不起。背景:我正在编写一个程序,它使用Lua解释器作为用户执行指令的基础,并使用根库(https://root.cern.ch/)显示图形、直方图等.所有这些都工作得很好,但随后我尝试实现一种方法,让用户在Lua提示符中输入命令,在任务完成时能够完全完成其他任务,或者请求停止它。我的第一次尝试如下:首先在Lua端加载一些助手函数并初始化全局变量
-- Lua script
RootTasks = {}
NextTaskToStart = nil
function SetupNewTask(taskname, fn, ...)
local task = function(...)
local rets = table.pack(fn(...))
RootTasks[taskname].status = "done"
return table.unpack(rets)
end
RootTasks[taskname] = {
task = SetupNewTask_C(task, ...),
status = "waiting",
}
NextTaskToStart = taskname
end然后在C面
// inside the C++ script
int SetupNewTask_C ( lua_State* L )
{
// just a function to check if the argument is valid
if ( !CheckLuaArgs ( L, 1, true, "SetupNewTask_C", LUA_TFUNCTION ) ) return 0;
int nvals = lua_gettop ( L );
lua_newtable ( L );
for ( int i = 0; i < nvals; i++ )
{
lua_pushvalue ( L, 1 );
lua_remove ( L, 1 );
lua_seti ( L, -2, i+1 );
}
return 1;
}基本上,用户提供要执行的函数,然后是要传递的参数,它只是推送一个表,函数作为第一个字段执行,参数作为后续字段。这个表被推到堆栈的顶部,我检索它并将它存储为一个全局变量。下一步是在Lua方面。
-- Lua script
function StartNewTask(taskname, fn, ...)
SetupNewTask(taskname, fn, ...)
StartNewTask_C()
RootTasks[taskname].status = "running"
end在C面
// In the C++ script
// lua, called below, is a pointer to the lua_State
// created when starting the Lua interpreter
void* NewTaskFn ( void* arg )
{
// helper function to get global fields from
// strings like "something.field.subfield"
// Retrieve the name of the task to be started (has been pushed as
// a global variable by previous call to SetupNewTask_C)
TryGetGlobalField ( lua, "NextTaskToStart" );
if ( lua_type ( lua, -1 ) != LUA_TSTRING )
{
cerr << "Next task to schedule is undetermined..." << endl;
return nullptr;
}
string nextTask = lua_tostring ( lua, -1 );
lua_pop ( lua, 1 );
// Now we get the actual table with the function to execute
// and the arguments
TryGetGlobalField ( lua, ( string ) ( "RootTasks."+nextTask ) );
if ( lua_type ( lua, -1 ) != LUA_TTABLE )
{
cerr << "This task does not exists or has an invalid format..." << endl;
return nullptr;
}
// The field "task" from the previous table contains the
// function and arguments
lua_getfield ( lua, -1, "task" );
if ( lua_type ( lua, -1 ) != LUA_TTABLE )
{
cerr << "This task has an invalid format..." << endl;
return nullptr;
}
lua_remove ( lua, -2 );
int taskStackPos = lua_gettop ( lua );
// The first element of the table we retrieved is the function so the
// number of arguments for that function is the table length - 1
int nargs = lua_rawlen ( lua, -1 ) - 1;
// That will be the function
lua_geti ( lua, taskStackPos, 1 );
// And the arguments...
for ( int i = 0; i < nargs; i++ )
{
lua_geti ( lua, taskStackPos, i+2 );
}
lua_remove ( lua, taskStackPos );
// I just reset the global variable NextTaskToStart as we are
// about to start the scheduled one.
lua_pushnil ( lua );
TrySetGlobalField ( lua, "NextTaskToStart" );
// Let's go!
lua_pcall ( lua, nargs, LUA_MULTRET, 0 );
}
int StartNewTask_C ( lua_State* L )
{
pthread_t newTask;
pthread_create ( &newTask, nullptr, NewTaskFn, nullptr );
return 0;
}因此,例如在Lua解释器中调用
> StartNewTask("PeriodicPrint", function(str) for i=1,10 print(str);
>> sleep(1); end end, "Hello")将在接下来的10秒钟内每秒钟产生一张“你好”的印刷品。然后它将从执行回来,一切都是美好的。现在,如果我在运行任务时按ENTER键,程序就会在可怕的seg错误痛苦中死亡(我不会在这里复制,因为每次出错日志都是不同的,有时根本没有错误)。所以我在网上读到了一些东西,我发现有几处提到lua_State并不是线程安全的。我真的不明白为什么仅仅打回车会使它崩溃,但这并不是真正的重点。
我偶然发现,这种方法只要稍加修改,就可以在没有任何故障的情况下工作。如果执行了协同线,而不是直接运行函数,我上面写的所有内容都能正常工作。
将以前的Lua边函数SetupNewTask替换为
function SetupNewTask(taskname, fn, ...)
local task = coroutine.create( function(...)
local rets = table.pack(fn(...))
RootTasks[taskname].status = "done"
return table.unpack(rets)
end)
local taskfn = function(...)
coroutine.resume(task, ...)
end
RootTasks[taskname] = {
task = SetupNewTask_C(taskfn, ...),
routine = task,
status = "waiting",
}
NextTaskToStart = taskname
end我可以一次执行几个任务,持续很长一段时间,不会出现任何故障。所以我们终于来问我的问题:为什么要使用协同工作?在这种情况下,根本的区别是什么?我只打电话给coroutine.resume,我不会做任何让步(或者其他任何重要的事情)。然后等待协同线完成,就这样了。coroutine在做我不怀疑的事情吗?
发布于 2017-10-06 06:52:59
似乎什么都没坏并不意味着它真的能工作,所以…
lua_State里有什么
(这就是协同线的含义。)
lua_State存储这个协同线的状态-最重要的是它的堆栈、CallInfo列表、指向global_State的指针以及其他一些东西。
如果在标准Lua解释器的REPL中单击“返回”,解释器将尝试运行您键入的代码。(空行也是一个程序。)这包括将它放在Lua堆栈上,调用一些函数等等。如果您的代码运行在不同的OS线程中,也使用相同的Lua堆栈/状态…。好吧,我觉得这很清楚为什么会这样,对吧?(问题的一部分是缓存“不”/不应该更改的内容(但是由于另一个线程也在破坏它)。两个线程都在同一个堆栈上推/弹出东西,并相互踩在一起。如果您想深入研究代码,luaV_execute可能是一个很好的起点。)
所以现在你使用两个不同的协同线,所有明显的问题来源都消失了。现在起作用了,对吗,…?不,因为合作伙伴共享状态,
这就是“注册表”、字符串缓存和所有与垃圾收集相关的内容的所在。而且,虽然您摆脱了主要的“高频”错误源(堆栈处理),许多其他“低频”源仍然存在。简短(并非详尽无遗!)其中一些人的名单:
GCdebt计数器是全局状态的一部分,因此一旦超过阈值,同时对多个线程的分配就很有可能同时在多个线程上启动GC。(如果发生这种情况,它几乎肯定会爆炸。)任何分配都意味着,除其他外- creating tables, coroutines, userdata, …
- concatenating strings, reading from files, `tostring()`, …
- _calling_ functions(!) (if that requires growing the stack or allocating a new `CallInfo` slot)
- etc.
__gc或__mode,则会将其添加到列表中。)然后可能还有很多其他的事情--…
使它失败
为了好玩,您可以重新构建Lua和#define - HARDSTACKTESTS和HARDMEMTESTS (例如,在luaconf.h的最顶端)。这将启用一些代码,这些代码将重新分配堆栈并在许多地方运行完整的GC循环。(对我来说,它只需打开提示符就可以重新部署260个堆栈和235个集合。只要按下返回(运行一个空程序),就可以实现13个堆栈重新分配和6个集合。运行您的程序,如果启用了该功能,可能会使其崩溃…。或者不是?
为什么它可能仍然“起作用”
因此,例如在Lua解释器中调用 StartNewTask("PeriodicPrint",函数(Str),用于i=1,10打印(Str);睡眠(1);结尾,"Hello") 将在接下来的10秒钟内每秒钟产生一张“你好”的印刷品。
在这个特殊的例子中,并没有发生什么事情。在启动线程之前,将分配所有函数和字符串。如果没有HARDSTACKTESTS,您可能会很幸运,而且堆栈已经足够大。即使堆栈需要增长,分配(&集合周期,因为HARDMEMTESTS)可能有正确的时间,这样它就不会严重中断。但是你的测试程序做的“真正的工作”越多,它就越有可能崩溃。(一种很好的方法是创建大量的表和东西,这样GC就需要更多的时间来完成整个周期,而有趣的比赛条件的时间窗口就变得更大了。或者只是重复运行一个虚拟函数,就像在2+线程上运行2+一样快,并希望得到最好的…错误,最糟糕的。)
https://stackoverflow.com/questions/46590270
复制相似问题