You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Lua中注册回调:实现Lua函数定时调用的技术咨询

问题背景与需求

架构说明(欢迎提出架构批评)

  • 现有大型C++程序,承载大量任务
  • 引入Lua实现脚本化功能,大量底层命令由Lua代码片段处理
  • 每个command关联一段Lua代码片段,执行command时触发对应片段执行
  • Lua代码片段可回调底层C++函数,此逻辑不影响核心需求
  • Lua代码片段在command启动后立即退出
  • command实际在外部执行(如视频播放/录制启停、倒带等),程序需主动轮询其状态,无回调、join机制

核心问题

部分Lua代码片段场景下,需要调度特定Lua函数的定时调用:

  • 单次调用:处理超时逻辑
  • 循环调用:处理外部command的状态轮询
    计划通过C启动线程实现定时,但不清楚如何传递Lua函数信息给C,以及如何在C++中调用该Lua函数

当前Lua类代码(未实现定时回调)

static inline bool is_whitespace(const std::string& s) {
    return std::all_of(s.begin(), s.end(), isspace);
}

class Lua {
    lua_State *state;
    std::map<std::string, int> fragments;

    void push(std::string const& str) {  lua_pushstring(state, str.c_str()); }
    void push(char const* str)        {  lua_pushstring(state, str);         }
    void push(bool value)             { lua_pushboolean(state, value);       }
    void push(uint32_t value)         { lua_pushinteger(state, value);       }
    void push(int value)              { lua_pushinteger(state, value);       }
    void push(double value)           {  lua_pushnumber(state, value);       }

public:
    void push(std::nullptr_t) = delete;

    Lua() : state(luaL_newstate()) {
        luaL_openlibs(state);
    }

    void register_c_func(const char *name, lua_CFunction func) {
        lua_register(state, name, func);
    }

    bool is_snippet(const std::string &name) {
        return fragments.count(name);
    }

    int register_snippet(const char *name, const char *fragment) {
        if (is_snippet(name)) {
            SPDLOG_ERROR("lua::register_snippet({}): ERROR name already present", name);
            return LUA_NOREF;
        }
        if (!is_whitespace(fragment)) {
            auto ret = luaL_loadstring(state, fragment);
            if (ret != LUA_OK) {
                SPDLOG_ERROR("lua::register_snippet({}): Lua registration failed ({})");
            } else {
                auto r = luaL_ref(state, LUA_REGISTRYINDEX);
                SPDLOG_TRACE("lua::register_snippet({}, {}): {}", name, fragment, r);
                fragments[name] = r;
                return r;
            }
        } else {
            SPDLOG_WARN("lua::register_snippet({}): has empty Lua fragment", name);
        }
        return LUA_NOREF;
    }

    template<typename... Args> bool exec(const std::string &name, Args... args){
        if (is_snippet(name)) {
            lua_rawgeti(state, LUA_REGISTRYINDEX, fragments.at(name));
            ((push(std::forward<Args>(args))), ...);
            int nargs = sizeof ...(Args);
            SPDLOG_TRACE("lua::exec({}, {}): {}", name, fragments.at(name), nargs);
            lua_pcall(state, nargs, LUA_MULTRET, 0);
            return true;
        } else {
            SPDLOG_WARN("lua::exec({}): has no Lua fragment", name);
            return false;
        }
    }

    void exec(const char* fragment) {
        SPDLOG_TRACE("lua::exec(): '{}'", fragment);
        auto ret = luaL_dostring(state, fragment);
        if (ret)
            SPDLOG_ERROR("lua::exec(): ERROR {}", ret);
    }

};

extern Lua master_lua;

注:Lua初始化、C++函数注册及代码片段注册不在此代码范围内


解决方案

核心思路

利用Lua注册表存储定时函数的引用(整数索引),C++侧维护定时任务队列,通过线程触发Lua函数调用。关键要点:

  1. Lua函数可通过luaL_ref存入注册表,得到的整数索引可安全传递给C++
  2. Lua状态机非线程安全,所有Lua API调用必须在同一线程,或通过互斥锁同步
  3. 定时任务需包含函数索引、延迟/间隔、循环标记,以及可选的取消标识

步骤1:扩展Lua类,添加线程安全调用支持

给Lua类添加状态机访问、互斥锁保护及注册表函数调用逻辑:

class Lua {
    // ... 原有代码 ...

    // 添加互斥锁保护Lua状态机访问
    std::mutex mtx;

public:
    // ... 原有代码 ...

    // 获取Lua状态机(需配合互斥锁使用)
    lua_State* get_state() { return state; }

    // 获取互斥锁,用于线程安全访问
    std::mutex& get_mutex() { return mtx; }

    // 封装调用注册表中函数的方法
    bool call_registry_func(int ref, int nargs = 0, int nresults = LUA_MULTRET) {
        std::lock_guard<std::mutex> lock(mtx);
        lua_rawgeti(state, LUA_REGISTRYINDEX, ref);
        int ret = lua_pcall(state, nargs, nresults, 0);
        if (ret != LUA_OK) {
            SPDLOG_ERROR("Lua function call failed: {}", lua_tostring(state, -1));
            lua_pop(state, 1);
            return false;
        }
        lua_pop(state, nresults);
        return true;
    }

    // 释放注册表中的函数引用
    void unref(int ref) {
        std::lock_guard<std::mutex> lock(mtx);
        luaL_unref(state, LUA_REGISTRYINDEX, ref);
    }
};

步骤2:定义定时任务结构

在C++中定义存储定时任务信息的结构体:

#include <chrono>
#include <atomic>

struct ScheduledTask {
    int lua_func_ref;          // Lua注册表中的函数索引
    std::chrono::milliseconds delay; // 首次执行延迟
    std::chrono::milliseconds interval; // 循环执行间隔(单次任务设为0)
    std::atomic<bool> canceled; // 任务取消标记
    std::chrono::steady_clock::time_point next_run; // 下次执行时间

    ScheduledTask(int ref, std::chrono::milliseconds d, std::chrono::milliseconds i)
        : lua_func_ref(ref), delay(d), interval(i), canceled(false) {
        next_run = std::chrono::steady_clock::now() + delay;
    }
};

步骤3:实现定时任务调度器

创建单例调度器,维护任务队列并启动处理线程:

#include <queue>
#include <thread>
#include <condition_variable>

class TaskScheduler {
private:
    std::priority_queue<
        ScheduledTask*,
        std::vector<ScheduledTask*>,
        // 按next_run升序排列,优先执行最早的任务
        std::function<bool(ScheduledTask*, ScheduledTask*)>
    > task_queue{
        [](ScheduledTask* a, ScheduledTask* b) {
            return a->next_run > b->next_run;
        }
    };
    std::mutex queue_mtx;
    std::condition_variable cv;
    std::atomic<bool> running{true};
    std::thread worker_thread;

    void worker_loop() {
        while (running) {
            std::unique_lock<std::mutex> lock(queue_mtx);
            cv.wait(lock, [this]() {
                return !task_queue.empty() || !running;
            });

            if (!running && task_queue.empty()) break;

            auto now = std::chrono::steady_clock::now();
            auto task = task_queue.top();

            if (task->next_run > now) {
                cv.wait_until(lock, task->next_run);
                continue;
            }

            task_queue.pop();
            lock.unlock();

            if (task->canceled) {
                master_lua.unref(task->lua_func_ref);
                delete task;
                continue;
            }

            master_lua.call_registry_func(task->lua_func_ref);

            if (task->interval.count() > 0 && !task->canceled) {
                task->next_run = std::chrono::steady_clock::now() + task->interval;
                std::lock_guard<std::mutex> lock2(queue_mtx);
                task_queue.push(task);
                cv.notify_one();
            } else {
                master_lua.unref(task->lua_func_ref);
                delete task;
            }
        }
    }

public:
    TaskScheduler() {
        worker_thread = std::thread(&TaskScheduler::worker_loop, this);
    }

    ~TaskScheduler() {
        running = false;
        cv.notify_all();
        if (worker_thread.joinable()) {
            worker_thread.join();
        }
        std::lock_guard<std::mutex> lock(queue_mtx);
        while (!task_queue.empty()) {
            auto task = task_queue.top();
            task_queue.pop();
            master_lua.unref(task->lua_func_ref);
            delete task;
        }
    }

    // 注册定时任务:返回任务指针用于取消
    ScheduledTask* schedule_task(int lua_func_ref, std::chrono::milliseconds delay, std::chrono::milliseconds interval = std::chrono::milliseconds(0)) {
        auto task = new ScheduledTask(lua_func_ref, delay, interval);
        std::lock_guard<std::mutex> lock(queue_mtx);
        task_queue.push(task);
        cv.notify_one();
        return task;
    }

    // 取消定时任务
    void cancel_task(ScheduledTask* task) {
        if (task) {
            task->canceled = true;
            cv.notify_one();
        }
    }
};

extern TaskScheduler task_scheduler;

步骤4:注册C++函数到Lua,暴露调度接口

实现Lua可调用的C函数,用于注册和取消定时任务:

// Lua调用此函数注册定时任务
// 参数:func(要定时调用的Lua函数), delay_ms(延迟毫秒数), interval_ms(循环间隔,可选,0为单次)
static int lua_schedule_task(lua_State* L) {
    if (!lua_isfunction(L, 1)) {
        lua_pushstring(L, "first argument must be a function");
        lua_error(L);
    }
    if (!lua_isnumber(L, 2)) {
        lua_pushstring(L, "second argument must be a number (delay ms)");
        lua_error(L);
    }
    int interval_ms = lua_isnumber(L, 3) ? lua_tointeger(L, 3) : 0;

    lua_pushvalue(L, 1);
    int ref = luaL_ref(L, LUA_REGISTRYINDEX);

    auto delay = std::chrono::milliseconds(lua_tointeger(L, 2));
    auto interval = std::chrono::milliseconds(interval_ms);
    auto task = task_scheduler.schedule_task(ref, delay, interval);

    lua_pushlightuserdata(L, task);
    return 1;
}

// Lua调用此函数取消定时任务
// 参数:task(之前返回的任务指针)
static int lua_cancel_task(lua_State* L) {
    auto task = static_cast<ScheduledTask*>(lua_touserdata(L, 1));
    if (task) {
        task_scheduler.cancel_task(task);
    }
    return 0;
}

// 在Lua初始化时注册这两个函数
// master_lua.register_c_func("schedule_task", lua_schedule_task);
// master_lua.register_c_func("cancel_task", lua_cancel_task);

步骤5:Lua侧使用示例

在Lua代码片段中调用注册的函数实现定时逻辑:

-- 单次定时调用:1秒后执行超时处理
local timeout_task = schedule_task(function()
    print("Timeout triggered, stopping command...")
    stop_command() -- 调用C++函数停止command
end, 1000)

-- 循环轮询:每500毫秒检查command状态
local poll_task = schedule_task(function()
    local status = check_command_status() -- 调用C++函数获取状态
    print("Command status:", status)
    if status == "finished" then
        cancel_task(poll_task) -- 任务完成,取消轮询
        print("Command finished, stopping poll")
    end
end, 0, 500)

架构补充建议

  1. 线程安全:所有Lua状态机操作必须通过互斥锁保护,避免多线程同时访问导致崩溃
  2. 资源清理:当command结束时,必须取消所有关联的定时任务,避免无效调用和内存泄漏
  3. 错误处理:Lua函数调用失败时,需记录错误日志并清理相关资源
  4. 性能优化:如果定时任务数量较多,可考虑使用基于IO复用的定时器替代线程轮询

内容的提问来源于stack exchange,提问作者ZioByte

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 03:30:53