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

在Rust中使用mlua终止Lua脚本/线程的正确方法

如何在mlua脚本执行中途停止对应的Rust线程

我正在用mlua和Rust构建脚本引擎,同一时间仅运行一个脚本。计划为每个脚本启动独立线程,线程无返回值,可无限运行直至用户手动停止。当前遇到的问题是:如何在脚本执行中途停止该线程?

示例无限循环Lua脚本:

while true do
    log("I'm stuck in an infinite loop!")
end

当前Rust代码实现:

pub struct Script {
    name: String,
    path: PathBuf,
    lua: Arc<Mutex<Lua>>,
    thread: ThreadManager,
    should_stop: Arc<AtomicBool>,
}

impl Script {
    pub fn new(name: &str, path: PathBuf) -> Result<Self> {
        Ok(Self {
            name: name.to_string(),
            path,
            lua: Arc::new(Mutex::new(Lua::new())),
            thread: ThreadManager::new(),
            should_stop: Arc::new(AtomicBool::new(false)),
        })
    }

    pub fn load(&mut self) -> Result<()> {
        let lua = self.lua.lock().unwrap();
        let globals = lua.globals();

        // register API functions here
        globals.set(
            "log",
            lua.create_function(|_, msg: String| {
                info!("{}", msg);
                Ok(())
            })?,
        )?;

        info!("Loaded script: {}", self.name);
        Ok(())
    }

    pub fn start(&mut self) -> Result<()> {
        let script_content = std::fs::read_to_string(&self.path);
        let script_content = match script_content {
            Ok(content) => content,
            Err(_) => {
                return Err(Error::RuntimeError(
                    "failed to read script file".to_string(),
                ));
            }
        };
        let lua_arc_clone = Arc::clone(&self.lua);
        let thread_state = self.thread.get_state(&self.name);
        if thread_state == ThreadState::Stopped {
            let is_started = self.thread.start(&self.name, move || {
                let lua = lua_arc_clone.lock().unwrap();
                lua.load(&script_content).exec().unwrap();
            });
            if !is_started {
                warn!("failed to start thread for: {}", self.name);
            }
        }
        Ok(())
    }

    pub fn stop(&mut self) -> Result<()> {
        let thread_state = self.thread.get_state(&self.name);
        if thread_state == ThreadState::Running {
            self.thread.stop(&self.name);
            info!("script stopped: {}", self.name);
        }
        Ok(())
    }
}

线程启动逻辑:

pub fn start<T>(&mut self, name: &str, t: T) -> bool
where
    T: Fn() + Send + 'static,
{
    let state = Arc::new(Mutex::new(ThreadState::Running));
    let state_clone = Arc::clone(&state);

    thread::spawn(move || loop {
        match *state_clone.lock().unwrap() {
            ThreadState::Running => {
                info!("Running thread");
                t();
            }
            ThreadState::Paused => {
                info!("Paused thread");
                thread::sleep(Duration::from_millis(10));
            }
            ThreadState::Stopped => {
                info!("Stopped thread");
                break;
            }
        }
    });

    self.threads.insert(name.clone().to_string(), state);

    true
}

此前尝试用信号实现,但Lua的执行会阻塞状态检查,无法生效。


解决方案

直接强制终止Rust线程会导致Lua虚拟机资源泄漏、锁持有等不安全问题,推荐利用mlua的协程钩子机制实现安全中断:

方案:使用mlua钩子定期检查停止信号

核心思路是让Lua虚拟机在执行过程中定期触发钩子,检查停止标记,若标记为true则抛出错误终止脚本执行。

1. 修改Script的load方法,添加钩子

pub fn load(&mut self) -> Result<()> {
    let lua = self.lua.lock().unwrap();
    let globals = lua.globals();

    // 注册log函数
    globals.set(
        "log",
        lua.create_function(|_, msg: String| {
            info!("{}", msg);
            Ok(())
        })?,
    )?;

    // 克隆停止标记供钩子使用
    let should_stop_clone = Arc::clone(&self.should_stop);
    // 设置钩子:每执行100条Lua指令触发一次检查
    lua.set_hook(mlua::HookTriggers::COUNT, 100, move |_| {
        if should_stop_clone.load(Ordering::SeqCst) {
            // 抛出错误终止Lua执行
            Err(mlua::Error::ExternalError(Box::new("Script stopped by user")))
        } else {
            Ok(())
        }
    })?;

    info!("Loaded script: {}", self.name);
    Ok(())
}

2. 修改start方法,处理脚本中断错误

pub fn start(&mut self) -> Result<()> {
    let script_content = std::fs::read_to_string(&self.path)?;
    let lua_arc_clone = Arc::clone(&self.lua);
    let should_stop_clone = Arc::clone(&self.should_stop);
    // 启动前重置停止标记
    should_stop_clone.store(false, Ordering::SeqCst);

    let thread_state = self.thread.get_state(&self.name);
    if thread_state == ThreadState::Stopped {
        let is_started = self.thread.start(&self.name, move || {
            let lua = lua_arc_clone.lock().unwrap();
            // 执行脚本并捕获钩子抛出的中断错误
            match lua.load(&script_content).exec() {
                Ok(_) => {},
                Err(e) => {
                    if e.to_string().contains("Script stopped by user") {
                        info!("Script stopped via hook: {}", self.name);
                    } else {
                        error!("Script execution error: {}", e);
                    }
                }
            }
        });
        if !is_started {
            warn!("failed to start thread for: {}", self.name);
        }
    }
    Ok(())
}

3. 修改stop方法,设置停止标记

pub fn stop(&mut self) -> Result<()> {
    let thread_state = self.thread.get_state(&self.name);
    if thread_state == ThreadState::Running {
        // 设置停止标记,钩子会检测到并终止脚本
        self.should_stop.store(true, Ordering::SeqCst);
        // 通知线程管理器切换状态
        self.thread.stop(&self.name);
        info!("script stopped: {}", self.name);
    }
    Ok(())
}

注意事项

  • 钩子触发频率可调整:100代表每执行100条Lua指令触发一次,频率过高会影响性能,过低会导致停止响应延迟
  • 禁止直接强制终止线程:会导致Lua虚拟机处于不一致状态,引发内存泄漏或死锁
  • 确保所有自定义Lua绑定函数不长期阻塞,否则会延迟中断响应

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 18:05:05