基于NLua实现使用lua脚本中多线程执行方法

Posted lishuangquan1987

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于NLua实现使用lua脚本中多线程执行方法相关的知识,希望对你有一定的参考价值。

背景

C#+Lua的交互的库有很多,比如常见的NLua,XLua等,在github上可以搜到,但是我个人还是喜欢用NLua.对于很多工业自动化控制项目,使用C#+Lua+插件开发模式,简直完美
由于有使用Lua脚本启动线程去同时执行多个方法的需求,所以这里将研究C#+Lua基于多虚拟机的机制来实现在Lua脚本中用线程去执行Lua方法所踩过的坑记录下来。此文章是建立在有一定C#和Lua交互的知识上的,如果没有相关的经验,可以参考参考以前的文章:C#+NLua实现将Lua代码在主线程上执行

错误的认知

之前以为,在Lua中多线程执行方法不是很简单的事情吗?在C#中注册一个全局方法,然后C#中New一个线程去执行,在Lua中调用这个方法就行。请看如下的例子:

C#代码:

public class Program


    public static void Main(string[] args)
    
        Console.WriteLine("Hello, World!");

        NLua.Lua lua = new NLua.Lua();
        lua.State.Encoding = System.Text.Encoding.Default;
        lua.LoadCLRPackage();
        //注册方法
        lua.RegisterFunction("ExecuteInNewThread", typeof(Program).GetMethod("ExecuteInNewThread"));
        lua.RegisterFunction("Delay", typeof(Thread).GetMethod("Sleep", new Type[]  typeof(int) ));
        lua.RegisterFunction("Print", typeof(Console).GetMethod("WriteLine", new Type[]  typeof(object) ));
        //调用脚本
        lua.DoFile("Test.lua");
        //执行脚本中的方法
        lua.DoString("ThreadTest();");
        Console.ReadLine();
    
    public static void ExecuteInNewThread(Action action)
    
        Task.Factory.StartNew(action);
    

lua脚本Test.lua的内容:

function ThreadTest( ... )
	-- body
	ExecuteInNewThread(function() 
						 for i=1,10 do
							Delay(500)
							Print(i)
						 end
					   end)

	ExecuteInNewThread(function() 
						for j=1,10 do
						Delay(500)
						Print(j)
						end
					end)
end

运行:

WTF,竟然报错,程序没有写错啊。翻译错误的意思大致是试图读取或写入受保护的内存。这通常表示其他内存已损坏
具体查看NLua的源码,比如DoString方法:

 public object[] DoString(string chunk, string chunkName = "chunk")
	
	    int num = _luaState.GetTop();
	    _executing = true;
	    if (_luaState.LoadString(chunk, chunkName) != 0)
	    
	        ThrowExceptionFromError(num);
	    
	
	    int errorFunctionIndex = 0;
	    if (UseTraceback)
	    
	        errorFunctionIndex = PushDebugTraceback(_luaState, 0);
	        num++;
	    
	
	    try
	    
	        if (_luaState.PCall(0, -1, errorFunctionIndex) != 0)
	        
	            ThrowExceptionFromError(num);
	        
	
	        return _translator.PopValues(_luaState, num);
	    
	    finally
	    
	        _executing = false;
	    
	

步骤是
申请堆栈(GetTop)->执行脚本(PCall)->从新堆栈中获取返回值(PopValues)
这样的过程都与一个变量息息相关_luaState,从代码中可以看到执行DoString方法没有任何地方加了锁,而每次New一个NLua.Lua对象,内部只有一个_luaState.当然加锁了也只是保证线程安全,并不能保证能并行执行,试想两个方法并行执行,两个方法执行的时候同时申请堆栈,同时执行,同时把返回值放入增长后的堆栈,那这两个方法的返回值哪个是哪个肯定会出现混淆,而且Lua的PCall方法也不允许多线程执行,看注释就知道了:Calls a function in protected mode,执行方法的时候,会把这块内存锁定起来,不让进行其他操作,如果有其他操作,会报错

//
// 摘要:
//     Calls a function in protected mode.
//
// 参数:
//   arguments:
//
//   results:
//
//   errorFunctionIndex:
public LuaStatus PCall(int arguments, int results, int errorFunctionIndex)

    return (LuaStatus)NativeMethods.lua_pcallk(_luaState, arguments, results, errorFunctionIndex, IntPtr.Zero, IntPtr.Zero);

正确的方式

前面说了,每次New一个Lua相当于新建一个Lua虚拟机,里面只有一个_luaState,这个_luaState只能单线程执行,那在Lua脚本中要多线程执行方法,改怎么办呢?
于是想到的解决办法是,new 多个备用的lua虚拟机,保证这几个虚拟机的环境一模一样(变量方法等),当要多线程执行lua方法时,把这个方法交给其他可用的lua虚拟机去执行。话不多说,照这个思路去敲代码:

    /// <summary>
    /// 可以执行多线程的lua虚拟机
    /// </summary>
    public class ThreadableLua
    
        private NLua.Lua mainLua;
        private List<NLua.Lua> backupLuas = new List<NLua.Lua>();
        private static object lockObj = new object();
        const string ExecutingThread = "IsExecutingThread";
        /// <summary>
        /// 需要再主线程上初始化
        /// </summary>
        /// <param name="maxThreadCount"></param>
        public ThreadableLua(int maxThreadCount = 3)
        
            mainLua = new NLua.Lua();
            mainLua["ID"] = 0;
            for (int i = 0; i < maxThreadCount; i++)
            
                var lua = new NLua.Lua();
                lua["ID"] = i + 1;
                lua[ExecutingThread] = false;//标识有没有正在执行线程
                backupLuas.Add(lua);
            
        

        public Encoding Encoding
        
            get  return mainLua.State.Encoding; 
            set
            
                mainLua.State.Encoding = value;
                foreach (var l in backupLuas)
                
                    l.State.Encoding = value;
                
            
        
        public void LoadCLRPackage()
        
            mainLua.LoadCLRPackage();
            foreach (var l in backupLuas)
            
                l.LoadCLRPackage();
            
        
        public object this[string fullPath]
        
            get  return mainLua[fullPath]; 
            set
            
                mainLua[fullPath] = value;
                foreach (var l in backupLuas)
                
                    l[fullPath] = value;
                
            
        
        /// <summary>
        /// 录入状态,所有的虚拟机都会录入
        /// </summary>
        /// <param name="chunk"></param>
        /// <param name="chunkName"></param>
        /// <returns></returns>
        public object[] DoString(string chunk, string chunkName = "chunk")
        
            foreach (var l in backupLuas)
            
                l.DoString(chunk, chunkName);
            
            return mainLua.DoString(chunk);
        
        public object[] DoFile(string fileName)
        
            foreach (var l in backupLuas)
            
                l.DoFile(fileName);
            
            return mainLua.DoFile(fileName);
        
        /// <summary>
        /// 执行脚本,只有一个虚拟机会去执行
        /// </summary>
        /// <param name="chunk"></param>
        /// <param name="chunkName"></param>
        /// <returns></returns>
        public object[] Execute(string chunk, string chunkName = "chunk")
        
            return mainLua.DoString(chunk);
        
        public NLua.LuaFunction RegisterFunction(string path, MethodBase function)
        
            foreach (var l in backupLuas)
            
                l.RegisterFunction(path, function);
            
            return mainLua.RegisterFunction(path, function);
        
        public NLua.Lua GetLuaCore()
        
            return mainLua;
        
        public void ExecuteInThread(string functionName, Action callback = null)
        
            lock (lockObj)//只能一个lua进入启动线程,防止并发
            
                NLua.Lua lua = null;
                foreach (var l in backupLuas)
                
                    if ((bool)l[ExecutingThread] == false)
                    
                        lua = l;
                        lua[ExecutingThread] = true;
                        break;
                    
                
                if (lua == null)
                
                    throw new Exception("没有可用的lua虚拟机");
                

                Task.Factory.StartNew(state =>
                
                    try
                    
                        var l = state as NLua.Lua;
                        var f = l[functionName] as LuaFunction;
                        f.Call();
                        l[ExecutingThread] = false;
                        callback?.Invoke();
                    
                    catch (Exception e)
                    
                    
                , lua);
            
        

    

使用:

    static ThreadableLua lua = new ThreadableLua();
    public static void Main(string[] args)
    
        Console.WriteLine("Hello, World!");

        
        lua.Encoding = System.Text.Encoding.Default;
        lua.LoadCLRPackage();
        //注册方法
        lua.RegisterFunction("ExecuteInNewThread", typeof(Program).GetMethod("ExecuteInNewThread"));
        lua.RegisterFunction("Delay", typeof(Thread).GetMethod("Sleep", new Type[]  typeof(int) ));
        lua.RegisterFunction("Print", typeof(Console).GetMethod("WriteLine", new Type[]  typeof(object) ));
        //调用脚本
        lua.DoFile("Test.lua");
        //执行脚本中的方法.这里不能使用DoString
        lua.Execute("ThreadTest();");
        Console.ReadLine();
    
    public static void ExecuteInNewThread(string functionName,Action callback)
    
        lua.ExecuteInThread(functionName, callback);
    

Test.lua脚本:

function ThreadTest( ... )
	-- body
	ExecuteInNewThread("Thread1",function() Print("thread1 finish") end)

	ExecuteInNewThread("Thread2",function() Print("thread2 finish") end)
end

function Thread1( ... )
	for i=1,10 do
		Delay(500)
		Print("thread1"..i)
	end
end

function Thread2( ... )
	for i=1,10 do
		Delay(500)
		Print("thread2"..i)
	end
end

运行结果如下:

正常输出无报错。
需要注意的是,Thread1和Thread2是在不同的虚拟机中执行的,因此他们在脚本中不共享变量,但是他们可以同时调用C#中的变量和方法来达到多线程运行的目的

以上是关于基于NLua实现使用lua脚本中多线程执行方法的主要内容,如果未能解决你的问题,请参考以下文章

Unity中C#与Lua的交互

Eclipse客户端程序中多线程的使用[1]

slua,ulua,nlua,unilua这几种unity3D的lua插件各有啥优劣

优惠卷秒杀系统设计秒杀优化 —— 基于阻塞队实现异步秒杀优化 及 基于Lua脚本判断秒杀库存一人一单

Lua脚本在Redis事务中的应用实践

2022年12月 .NET CORE工具案例-CSRedis执行Lua脚本实现商品秒杀