如果不执行协程,则在pthread seg-fault之间共享Lua状态

时间:2017-10-05 16:10:57

标签: c++ multithreading lua

首先,我知道我的问题看起来很熟悉,但我实际上并不是在为不同的pthread之间共享lua状态时问及为什么会发生seg-fault。我实际上问他们为什么不在下面描述的特定情况下出错。 我试着尽可能地组织它,但我意识到它很长。对于那个很抱歉。 一点背景: 我正在编写一个程序,它使用Lua解释器作为用户执行指令和使用ROOT库(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秒内生成&#34; Hello&#34;每一秒。它将从执行返回,一切都很美好。 现在,如果我在该任务运行时按下ENTER键,程序将死于可怕的段故障(我不会在这里复制,因为每次seg-fault错误日志都不同,有时候没有错误在所有)。 所以我在网上看了一下可能是什么问题,我发现有几个提到lua_State不是线程安全的。我真的不明白为什么只要点击ENTER就会让它翻转,但这并不是真的。

我偶然发现这种方法可以在没有任何细分修改的情况下工作。如果执行协程,而不是直接运行该函数,我上面写的所有内容都可以正常工作。

替换以前的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,我不做任何收益(或其他任何重要事项)。然后等待协程完成,那就是它。 协程是否做了我不怀疑的事情?

1 个答案:

答案 0 :(得分:1)

似乎似乎什么都没有破坏并不意味着它确实有效,所以......

lua_State中的内容是什么?

(这是一个协程。)

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

如果在the REPL of the standard Lua interpreter中点击返回,则解释程序会尝试运行您键入的代码。 (空行也是一个程序。)这涉及将它放在Lua堆栈上,调用一些函数等等。如果你的代码在不同的OS线程中运行,它也使用相同的Lua堆栈/状态......好吧,我觉得很清楚为什么会这样,对吧? (问题的一部分是缓存那些不会改变的东西(但是因为另一个线程也在搞乱它而改变)。两个线程都在推送/弹出东西在同一个堆栈上并踩在彼此的脚上。如果你想挖掘代码,luaV_execute可能是一个很好的起点。)

所以现在你正在使用两种不同的协程,所有明显的问题来源都消失了。现在它有效,对吧......?不,因为协同程序共享状态,

global_State

这是&#34;注册表&#34;,字符串缓存以及与垃圾收集相关的所有内容。而当你摆脱主要&#34;高频&#34;错误来源(堆栈处理),许多许多其他&#34;低频&#34;消息来源仍然其中一些的简短(非详尽!)列表:

  • 您可以通过任何分配触发垃圾收集步骤,然后将运行GC,使用其共享结构。虽然分配通常不会触发GC,但控制它的GCdebt计数器是全局状态的一部分,因此一旦超过阈值,就会在多个线程上进行分配。同时有机会一次在多个线程上启动GC。 (如果发生这种情况,它几乎肯定会暴力爆发。)任何分配意味着,等等

    • 创建表格,协同程序,userdata,...
    • 连接字符串,从文件中读取tostring(),...
    • 调用函数(!)(如果需要增加堆栈或分配新的CallInfo插槽)
  • (重新)设置事物的metatable可能会修改GC结构。 (如果元表具有__gc__mode,则会将其添加到列表中。)

  • 向表中添加新字段,这可能会触发调整大小。如果您在调整大小期间(甚至只是阅读现有字段)也从另一个线程访问它,那么...... * boom * 。 (或者不是繁荣,因为虽然数据可能已经移动到不同的区域,但之前的内存可能仍然可以访问。因此它可能会工作&#34;或者只会导致无声的腐败。)

  • 即使您停止了GC,创建新字符串也是不安全的,因为它可能会修改字符串缓存。

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

使其失败

为了好玩,您可以重新构建Lua和#define HARDSTACKTESTSHARDMEMTESTS(例如luaconf.h的顶部)。这将启用一些代码,这些代码将重新分配堆栈并在许多位置运行完整的GC循环。 (对我来说,它会执行260次堆栈重新分配和235次收集,直到它显示提示。只需返回(运行一个空程序)就会执行13次堆栈重新分配和6次收集。)运行您的程序似乎可以使用启用它的程序可能让它崩溃......或者不是吗?

为什么它仍然可以&#34;工作&#34;

  

例如,在Lua解释器中调用

StartNewTask("PeriodicPrint", function(str)
  for i=1,10  print(str); sleep(1);  end
end, "Hello")
     

将在接下来的10秒内生成&#34; Hello&#34;每一秒。

在这个特定的例子中,并没有发生太多事情。在启动线程之前分配所有函数和字符串。没有HARDSTACKTESTS,你可能很幸运,堆栈已经够大了。即使堆栈需要增长,分配(&amp;收集周期,因为HARDMEMTESTS)可能具有正确的时间,因此它不会破坏。但是更多&#34;真正的工作&#34;你的测试程序做的越多,它崩溃的可能性就越大。 (这样做的一个好方法是创建大量的表和内容,这样GC需要更多的时间来完成整个周期,有趣的竞争条件的时间窗口变得更大。或者可能只是反复运行虚拟函数,如{{1在2+线程上,希望最好......错误,最差。)