c++ - 如果不执行协程,则在pthread段故障之间共享Lua状态

标签 c++ multithreading lua

首先,我知道我的问题看起来很熟悉,但实际上我不是在问为什么在不同的pthread之间共享lua状态时会出现段错误。我实际上是在问为什么在下面描述的特定情况下他们不进行分段故障。
我尽力组织了它,但是我意识到它很长。对于那个很抱歉。
一点背景:
我正在编写一个程序,该程序使用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秒钟内,将每秒打印一次“Hello”。然后它将从执行中返回,一切都很棒。
现在,如果我在执行该任务时按ENTER键,程序将死于可怕的段错误(我在此不会复制此段错误,因为每次段错误时错误日志都不同,有时根本没有错误) )。
所以我在线上读了些什么可能是怎么回事,我发现有人提到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而我没有做任何产量(或其他任何重要事情)。然后,只需等待协程完成即可。
协程正在做我不怀疑的事情吗?

最佳答案

似乎什么也没发生并不意味着它确实有效,所以…

lua_State 中有什么?

(这就是协程。)
lua_State存储此协程的状态-最重要的是它的堆栈, CallInfo 列表,指向global_State的指针以及许多其他内容。

如果您在the REPL of the standard Lua interpreter中命中return,那么解释器将尝试运行您键入的代码。 (空行也是一个程序。)这涉及将其放在Lua堆栈上,调用某些函数等。如果您的代码在另一个OS线程中运行,并且该线程也使用相同的Lua堆栈/状态,那么……我认为很明显为什么会中断,对吗? (问题的一部分是缓存“不” /不应更改的内容的缓存(但更改是因为另一个线程也将其弄乱了)。两个线程都在同一堆栈中推送/弹出内容并相互踩踏如果您想深入研究代码,那么 luaV_execute 可能是一个不错的起点。)

因此,现在您使用的是两种不同的协程,所有明显的问题源均已消失。现在可以了,对吗……?不,因为协程共享状态,

global_State !

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

  • 您可以通过任何分配来触发垃圾回收步骤,这将使GC运行一段时间,并使用其共享结构。尽管分配通常不会触发GC,但是控制它的GCdebt计数器是全局状态的一部分,因此,一旦超过阈值,则同时在多个线程上进行分配就很有可能在多个线程上启动GC立刻。 (如果发生这种情况,几乎肯定会猛烈爆炸。)任何分配手段,除其他外,都意味着
  • 创建表,协程,用户数据,…
  • 连接字符串,从文件中读取,tostring(),...
  • 调用函数(!)(如果需要增加堆栈或分配新的CallInfo插槽)
  • (重新)设置事物的元表可能会修改GC结构。 (如果该元表具有__gc__mode,它将被添加到列表中。)
  • 向表中添加新字段,这可能会触发调整大小。如果您还在调整大小期间从另一个线程访问它(甚至只是读取现有字段),那么……*繁荣*。 (或者不是繁荣时期,因为尽管数据可能已经移到另一个区域,但以前的内存可能仍然可以访问。因此它可能“起作用”或仅导致无提示的损坏。)
  • 即使您停止了GC,创建新字符串也是不安全的,因为它可能会修改字符串缓存。

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

    失败

    为了娱乐,您可以同时重建#defineHARDSTACKTESTS的Lua和HARDMEMTESTS(例如在luaconf.h的最顶部)。这将启用一些代码,这些代码将重新分配堆栈并在许多地方运行完整的GC周期。 (对我来说,它会进行260个堆栈重新分配和235个集合,直到出现提示为止。只需按return键(运行一个空程序)就会执行13个堆栈重新分配和6个集合。)运行看起来与启用的功能相符的程序可能会使它崩溃...还是可能?

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

    So for instance a call in the Lua interpreter to

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

    Will produce for the next 10 seconds a print of "Hello" every second.



    在此特定示例中,发生的事情很少。在启动线程之前,将分配所有功能和字符串。没有HARDSTACKTESTS,您可能很幸运,而且堆栈已经足够大。即使堆栈需要增加,分配(&收集周期,因为HARDMEMTESTS)也可能具有正确的时间安排,因此不会造成可怕的破坏。但是测试程序所做的“实际工作”越多,崩溃的可能性就越大。 (执行此操作的一种好方法是创建大量表和内容,以便GC在整个周期中需要更多的时间,并且有趣的竞争条件所需的时间窗口也会变大。或者也许像2上的for i = 1, 1e9 do (function() return i end)() end一样快速地重复运行一个虚拟函数+线程,并希望获得最好的……错误,最糟糕。)

    关于c++ - 如果不执行协程,则在pthread段故障之间共享Lua状态,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46590270/

    相关文章:

    ios - NSURLConnection发生无法解释的崩溃

    c# - 用于命名旨在替换现有 API 的 C# 类/方法的建议

    c++ - 当在头文件而不是 CPP 文件上实现时,析构函数会导致内存泄漏 - 仅在 linux 上

    c++ - 单个指针是否可以指向两个或多个变量

    c++ - 如何像标准输入一样在Windows中的gcc中逐行读取命令输出?

    java - 多线程延迟的原因

    c++ - 在单个内核上运行的多个线程如何进行数据竞争?

    C++ 将已存在的对象实例公开给脚本语言

    c - 使用 Lua 进行日志记录....多次调用 luaL_openlibs 是一件坏事吗?

    android - Lua 可以单独用于应用程序开发吗?