基于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脚本中多线程执行方法的主要内容,如果未能解决你的问题,请参考以下文章
slua,ulua,nlua,unilua这几种unity3D的lua插件各有啥优劣