首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >线程seg之间的共享Lua状态-如果不执行coroutine则发生故障

线程seg之间的共享Lua状态-如果不执行coroutine则发生故障
EN

Stack Overflow用户
提问于 2017-10-05 16:10:56
回答 1查看 579关注 0票数 1

首先,我知道我的问题看起来很熟悉,但实际上我并不是在问为什么在不同的线程之间共享lua状态时会出现seg错误。我实际上是在问,为什么在下面描述的一个具体案例中,它们没有故障。我尽力把它组织得很好,但我意识到它很长。真对不起。背景:我正在编写一个程序,它使用Lua解释器作为用户执行指令的基础,并使用根库(https://root.cern.ch/)显示图形、直方图等.所有这些都工作得很好,但随后我尝试实现一种方法,让用户在Lua提示符中输入命令,在任务完成时能够完全完成其他任务,或者请求停止它。我的第一次尝试如下:首先在Lua端加载一些助手函数并初始化全局变量

代码语言:javascript
复制
-- 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面

代码语言:javascript
复制
// 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方面。

代码语言:javascript
复制
-- Lua script
function StartNewTask(taskname, fn, ...)
  SetupNewTask(taskname, fn, ...)
  StartNewTask_C()
  RootTasks[taskname].status = "running"
end

在C面

代码语言:javascript
复制
// 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解释器中调用

代码语言:javascript
复制
> StartNewTask("PeriodicPrint", function(str) for i=1,10 print(str);
>> sleep(1); end end, "Hello")

将在接下来的10秒钟内每秒钟产生一张“你好”的印刷品。然后它将从执行回来,一切都是美好的。现在,如果我在运行任务时按ENTER键,程序就会在可怕的seg错误痛苦中死亡(我不会在这里复制,因为每次出错日志都是不同的,有时根本没有错误)。所以我在网上读到了一些东西,我发现有几处提到lua_State并不是线程安全的。我真的不明白为什么仅仅打回车会使它崩溃,但这并不是真正的重点。

我偶然发现,这种方法只要稍加修改,就可以在没有任何故障的情况下工作。如果执行了协同线,而不是直接运行函数,我上面写的所有内容都能正常工作。

将以前的Lua边函数SetupNewTask替换为

代码语言:javascript
复制
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在做我不怀疑的事情吗?

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2017-10-06 06:52:59

似乎什么都没坏并不意味着它真的能工作,所以…

lua_State里有什么

(这就是协同线的含义。)

lua_State存储这个协同线的状态-最重要的是它的堆栈、CallInfo列表、指向global_State的指针以及其他一些东西。

如果在标准Lua解释器的REPL中单击“返回”,解释器将尝试运行您键入的代码。(空行也是一个程序。)这包括将它放在Lua堆栈上,调用一些函数等等。如果您的代码运行在不同的OS线程中,也使用相同的Lua堆栈/状态…。好吧,我觉得这很清楚为什么会这样,对吧?(问题的一部分是缓存“不”/不应该更改的内容(但是由于另一个线程也在破坏它)。两个线程都在同一个堆栈上推/弹出东西,并相互踩在一起。如果您想深入研究代码,luaV_execute可能是一个很好的起点。)

所以现在你使用两个不同的协同线,所有明显的问题来源都消失了。现在起作用了,对吗,…?不,因为合作伙伴共享状态,

global_State

这就是“注册表”、字符串缓存和所有与垃圾收集相关的内容的所在。而且,虽然您摆脱了主要的“高频”错误源(堆栈处理),许多其他“低频”源仍然存在。简短(并非详尽无遗!)其中一些人的名单:

  • 您可能会通过任何分配触发垃圾收集步骤,然后该分配将运行GC,该GC使用其共享结构。虽然分配通常不会触发GC,但控制这一点的GCdebt计数器是全局状态的一部分,因此一旦超过阈值,同时对多个线程的分配就很有可能同时在多个线程上启动GC。(如果发生这种情况,它几乎肯定会爆炸。)任何分配都意味着,除其他外
代码语言:javascript
复制
- 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结构。(如果元可使用__gc__mode,则会将其添加到列表中。)
  • 向表中添加新字段,这可能会触发调整大小。如果您在调整大小时也要从另一个线程访问它(甚至只是读取现有字段),那么…*砰*(或者没有爆炸,因为虽然数据可能移动到了不同的区域,但以前的内存可能仍然可以访问。因此,它可能“奏效”,或者只会导致无声的腐败。)
  • 即使您停止了GC,创建新的字符串也是不安全的,因为它可能修改字符串缓存。

然后可能还有很多其他的事情--…

使它失败

为了好玩,您可以重新构建Lua和#define - HARDSTACKTESTSHARDMEMTESTS (例如,在luaconf.h的最顶端)。这将启用一些代码,这些代码将重新分配堆栈并在许多地方运行完整的GC循环。(对我来说,它只需打开提示符就可以重新部署260个堆栈和235个集合。只要按下返回(运行一个空程序),就可以实现13个堆栈重新分配和6个集合。运行您的程序,如果启用了该功能,可能会使其崩溃…。或者不是?

为什么它可能仍然“起作用”

因此,例如在Lua解释器中调用 StartNewTask("PeriodicPrint",函数(Str),用于i=1,10打印(Str);睡眠(1);结尾,"Hello") 将在接下来的10秒钟内每秒钟产生一张“你好”的印刷品。

在这个特殊的例子中,并没有发生什么事情。在启动线程之前,将分配所有函数和字符串。如果没有HARDSTACKTESTS,您可能会很幸运,而且堆栈已经足够大。即使堆栈需要增长,分配(&集合周期,因为HARDMEMTESTS)可能有正确的时间,这样它就不会严重中断。但是你的测试程序做的“真正的工作”越多,它就越有可能崩溃。(一种很好的方法是创建大量的表和东西,这样GC就需要更多的时间来完成整个周期,而有趣的比赛条件的时间窗口就变得更大了。或者只是重复运行一个虚拟函数,就像在2+线程上运行2+一样快,并希望得到最好的…错误,最糟糕的。)

票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/46590270

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档