本人表达能力有限,所以文字描述不太清晰,我更习惯自己默默地造轮子,所以我只能尽力保证我给轮子可以被直接使用。
虽然不太会说,但有一些前提还是必要讲一下的:
直观的讲:lua并不支持多线程,任何尝试用lua做并发方案的人,都有病,没错,我自己也是。
lua有并发需求本身就是一件很鬼扯的事,本身会有这种需求,就说明可能在项目架构的大方向上,存在了问题。
我认为对于C/C++程序员来说,我们看中lua的地方是,它能够用最小的代价与C/C++交互,能够由C/C++去弥补它的不足,
所以,它能够包容所有对它理解程度不一样的C++程序员,
你不会用lua来解决一个问题,没关系,你懂得C/C++的办法,你给lua公开一套接口,说不定解决的还更完美。
虽然从另外一个角度来看,这是一种脱裤子放屁的行为,
但你得知道,维护C++代码的代价,与维护lua代码的代价是不同的,C++乱了,你自己解决起来可能都是大问题,而lua是不怕乱的,除非是写C++的人乱搞。
所以说这么多,我个人有这种需求的理由是:我正在用lua来做并发服务端的业务逻辑,已经跑题太多了,我只简单说一下:
C++实现套接字IO >> 传递json作为文本协议 >> 解析成lua table >> lua执行业务逻辑
在lua执行业务逻辑的过程中,就对共享数据有需求了,例如用户login之后的数据由lua来管理。
解决方案共两种:
1、基于lua_newthread创建lua子对象,重定义lua源码中的lua_lock与lua_unlock宏。
优点:这种方案的外表是完美无缺的。
缺点:降低lua的整体运行速度,因为我们使用了加锁的方式去维护lua的gc机制,并且我个人认为代价很大。
这种方案是我最初设计方案,但由于不能忍受一次请求上百次加锁操作,我最终放弃了这个方案。
我懒,不想把这种已经放弃掉的方案往这边搬了,如果非常有必要,劳您移驾传送门。
2、将共享数据存储在C/C++或一个公共的lua_State对象中,利用lua元表实现共享table存取逻辑。
优点:具有最高的可维护性,因为是基于lua api实现,和lua版本无关。
缺点:局限性最大。
这是我目前正在使用的方案,如下:
请注意,本例基于c++17标准实现,用到了 std::variant
否则,请自行实现支持hash的变体结构,或者使用一个公共lua_State对象来实现交互逻辑。
在Microsoft Visual Studio中,C++标准版本的设置在:项目 - 属性 - C/C++ - 语言 - C++语言标准中
使用g++时,可以在命令行中直接设置。
//lxsharelib.h #pragma once #include "lua/lua.hpp" /* lua_openxsharelib 向一个lua_State中提供一个名为:xshare的table 提供以下接口: xshare.new(key); -- 返回一个和key关联的共享table副本,如果指定的key是已存在的共享table,则引用它,否则就创建它。key类型可以是整数,浮点数,字符串 xshare.lock(xshare_table); -- 锁定共享table xshare.unlock(xshare_table); -- 解锁共享table xshare.mutex(xshare_table, func); --func是回调函数,也就是一种自动加解锁的模式,用来应对那种可能在函数中很多地方需要返回的加解锁处理的方案 示例: xshare.mutex(xst, function(_Tab) for i, e in pairs(_Tab) do print(i..‘:‘..e); end end); local ret = xshare.get(xshare_table); -- 将当前共享表的内容完整的复制到lua中,注意ret是一个正常table 示例: local xt = xshare.new(‘test share table‘); xt.d = {1,2,3}; local tab = xshare.get(xt); local tab_d = xshare.get(xt.d); xshare.set(xshare_table, table); -- 如果是要直接将根节点设置为一个表,可以使用这个方法,也就是它和xshare.get是反过来的作用 示例: local xt = xshare.new(‘test share table‘); xshare.set(xt, {d = {1,2,3}}); 不能将一个xshare_table设置为nil,参数2:table必须是一个有效的table,可以是空表,但不能是nil,理由如下: 本来我已经写好了xshare.delete,最终我删掉了,原因是,由lua删除共享表根节点会把事情变得复杂, 因为根节点必定是所有线程一起保存在各自的文件中,其中某个线程删除了根节点,其他线程要解决错误就非常蛋疼了。 需要删除子节点可以使其=nil,而会被我们删除的子节点,本身就不应该由我们的来保存它的副本。 所以我仅仅考虑了在C++里释放内存的方式:lua_closexsharelib,当所有lua对象已经88的时候,由C++调用它即可释放内存 */ int lua_openxsharelib(lua_State *s); int lua_closexsharelib();
//lxsharelib.cpp #include "pch.h" #include <mutex> #include <string> #include <unordered_map> #include <variant> #include "lxsharelib.h" class xshare_table; using xshare_bool = unsigned char; using xshare_value = std::variant<std::string, intptr_t, double, xshare_bool, xshare_table*>; using xshare_key = std::variant<std::string, intptr_t, double>; using xshare_type = std::unordered_map<xshare_key, xshare_value>; class xshare_table { public: xshare_table() {imax = 0;} ~xshare_table() { for (auto it = vars.begin(); it != vars.end(); ++it) { if (it->second.index() == 4) delete (std::get<4>(it->second)); } } std::recursive_mutex mtx; intptr_t imax; xshare_type vars; }; #define lua_isintnum(_Number_) (_Number_ - floor(_Number_) < 1e-6) /* 从lua栈上获取xshare_key */ int xshare_get_key(lua_State *s, int n, xshare_key &k) { int _Type = lua_type(s, n); switch (_Type) { case LUA_TNUMBER: { double dv = lua_tonumber(s, n); if (lua_isintnum(dv))//判断整数 k = (intptr_t)dv; else k = dv; break; } case LUA_TSTRING: k = lua_tostring(s, n); break; default: // 莫名奇妙的key时,返回-1,让上层去报错 return -1; } return 0; } /* 从lua的栈上,获取xshare_table的指针 */ xshare_table *xshare_ctab(lua_State *s) { if (!lua_gettop(s) || lua_type(s, 1) != LUA_TTABLE) { luaL_error(s, "invalid share_table"); return nullptr; } lua_pushstring(s, "__ptr_"); lua_gettable(s, 1); if (lua_type(s, -1) == LUA_TNIL) { // 获取 __ptr_失败时,报错 lua_pop(s, 1); luaL_error(s, "invalid share_table"); return nullptr; } xshare_table *_Result = (xshare_table *)lua_touserdata(s, -1); lua_pop(s, 1); return _Result; } /* 将share_table中的key返回给lua的push环节 */ void xshare_push_key(lua_State *s, const xshare_key &k) { switch (k.index()) { case 0://std::string lua_pushstring(s, std::get<0>(k).c_str()); break; case 1://intptr_t lua_pushinteger(s, std::get<1>(k)); break; case 2://double lua_pushnumber(s, std::get<2>(k)); break; } } /* 将share_table中的value返回给lua的push环节 */ void xshare_push_val(lua_State *s, const xshare_value &v) { switch (v.index()) { case 0://std::string lua_pushstring(s, std::get<0>(v).c_str()); break; case 1://intptr_t lua_pushinteger(s, std::get<1>(v)); break; case 2://double lua_pushnumber(s, std::get<2>(v)); break; case 3://xshare_bool(unsigned char) lua_pushboolean(s, std::get<3>(v)); break; case 4://xshare_table* lua_newtable(s); lua_pushstring(s, "__ptr_"); lua_pushlightuserdata(s, std::get<4>(v)); lua_settable(s, -3); lua_getglobal(s, "__xshare_object_metatable"); lua_setmetatable(s, -2); break; } } /* __index元方法 */ int lua_xshare_get(lua_State *s) { auto p = xshare_ctab(s); xshare_key k; if(xshare_get_key(s, 2, k)) luaL_error(s, "invalid xshare_key type:%s", lua_typename(s, lua_type(s, 2))); auto it = p->vars.find(k); if (it == p->vars.end()) return 0; xshare_push_val(s, it->second); return 1; } int xshare_set_tab(lua_State *s, xshare_table *t, int n); /* share_table.key = lua栈(n) */ int xshare_set_tabval(lua_State *s, xshare_table *t, xshare_key &key, int n) { auto it = t->vars.find(key); bool et = false; bool st = false; if (it != t->vars.end()) { if (it->second.index() == 4) st = true; et = true; } /* 如果是由__newindex调用,LUA_TNIL已经在上层判断过了 如果是由lua_next遍历table过来的调用,是不会获得nil的 所以这里不需要判断nil类型 */ int _Type = lua_type(s, n); switch (_Type) { case LUA_TBOOLEAN: if (et) { //已存在的key if (st) delete (std::get<4>(it->second));//value是xshare_table时,删除它 it->second = (xshare_bool)lua_toboolean(s, n); } else { t->vars[key] = (xshare_bool)lua_toboolean(s, n); } break; case LUA_TNUMBER: { double dv = lua_tonumber(s, n); if (et) { if (st) delete (std::get<4>(it->second)); if (lua_isintnum(dv)) it->second = (intptr_t)dv; else it->second = dv; } else { if (lua_isintnum(dv)) t->vars[key] = (intptr_t)dv; else t->vars[key] = dv; } break; } case LUA_TSTRING: { if (et) { if (st) delete (std::get<4>(it->second)); it->second = lua_tostring(s, n); } else { t->vars[key] = lua_tostring(s, n); } break; } case LUA_TTABLE: { xshare_table *_rt; if (et) { if (!st) { // 不是xshare_table时,预先创建它 _rt = new xshare_table; it->second = _rt; } else { // 已经是xshare_table了,就直接返回它 _rt = std::get<4>(it->second); } } else { _rt = new xshare_table; t->vars[key] = _rt; } return xshare_set_tab(s, _rt, ((n > 0) ? n : -2));//递归前进, n比0大时,直接传入n,否则直接传入-2(它包含了lua_pushnil,所以是-2) break; } default: return -1; } return 0; } int xshare_set_tab(lua_State *s, xshare_table *t, int n) { lua_pushnil(s); int _Result = 0; while (lua_next(s, n)) { xshare_key key; if (xshare_get_key(s, -2, key)) { //获取key错误时,清理栈并返回错误信息 _Result = (1 << 16) | lua_type(s, -2); lua_pop(s, 1); break; } if (key.index() == 1) { //整数key时,记录最大index intptr_t i = std::get<1>(key); if (i > t->imax) t->imax = i; } int rc = xshare_set_tabval(s, t, key, -1); // 由此处判断rc的理由是,在产生错误之后,让最上层的调用处去报告错误,这样就能在出错之后最起码保证lua栈还是正确的 if (rc == -1) { //设置数据错误时,如果返回值为-1,说明错误在当前这一层table里,此时应该清理栈并返回错误信息 _Result = (2 << 16) | lua_type(s, -2); lua_pop(s, 1); break; } else { //如果返回值不为0,说明错误在由更下层的table返回,此时应该直接清理栈并返回rc if (rc != 0) { _Result = rc; lua_pop(s, 1); break; } } lua_pop(s, 1); } return 0; } /* __newindex元方法 */ int lua_xshare_set(lua_State *s) { auto p = xshare_ctab(s); xshare_key key; if(xshare_get_key(s, 2, key)) luaL_error(s, "invalid xshare_key type:%s", lua_typename(s, lua_type(s, 2))); int vt = lua_type(s, 3); auto it = p->vars.find(key); if (it != p->vars.end()) { if (key.index() == 1) { intptr_t ikey = std::get<1>(it->first); if (vt == LUA_TNIL) { // 删除了最大的一个整数key时,让imax-1 if (p->imax == ikey) p->imax--; if (it->second.index() == 4) //如果被设置为nil的对象是一个xshare_table,删除它 delete (std::get<4>(it->second)); p->vars.erase(it); return 0; } //被设置的ikey比imax更大时,更改记录 if (ikey > p->imax) p->imax = ikey; } else { if (vt == LUA_TNIL) { if (it->second.index() == 4) //如果被设置为nil的对象是一个xshare_table,删除它 delete (std::get<4>(it->second)); p->vars.erase(it); return 0; } } } else { // 当key不存在时,如果value是nil,直接返回 // lua本身这个样子操作好像是要报错的,但这个报错我感觉意义并不大,也许这仅仅是我的感觉,如果要让它报错,删掉这个判断即可 if (vt == LUA_TNIL) return 0; if (key.index() == 1) { intptr_t ikey = std::get<1>(key); if (ikey > p->imax) p->imax = ikey; } } int rc = xshare_set_tabval(s, p, key, 3); if (rc == -1) { luaL_error(s, "invalid xshare value type:%s", lua_typename(s, lua_type(s, 3))); } else { if (rc != 0) { luaL_error(s, (((((unsigned int)rc) >> 16) & 0xFFFF) == 1) ? "invalid xshare_key type:%s" : "invalid xshare value type:%s", lua_typename(s, (((unsigned int)rc) & 0xFFFF))); } } return 0; } /* 给迭代器返回的next函数 */ int lua_xshare_next(lua_State *s) { auto p = xshare_ctab(s); auto it = p->vars.end(); if (lua_gettop(s) > 1 && lua_type(s, 2) != LUA_TNIL) { xshare_key key; if(xshare_get_key(s, 2, key)) luaL_error(s, "invalid xshare_key type:%s", lua_typename(s, lua_type(s, 2))); it = p->vars.find(key); } if (it != p->vars.end()) ++it; else it = p->vars.begin(); if (it == p->vars.end()) return 0; xshare_push_key(s, it->first); xshare_push_val(s, it->second); return 2; } /* __len元方法,也就是#tab */ int lua_xshart_getn(lua_State *s) { auto p = xshare_ctab(s); intptr_t j = p->imax; if (j > 0) { auto it = p->vars.find(j); if (it != p->vars.end()) { lua_pushinteger(s, j); return 1; } //这一段二叉查找整数key边界,我是直接从lua5.3中的ltable.c中luaH_getn里抄过来的, //在5.4中它又有了新的变化,预先存在了一系列路径判断,尽可能的考虑不去用哈希表搜索,但最坏的情况还是存在这段代码,在binsearch函数中 intptr_t i = 0; while (j - i > 1) { intptr_t m = (i + j) >> 1; if (p->vars.find(m) == p->vars.end()) j = m; else i = m; } p->imax = i; lua_pushinteger(s, i); return 1; } lua_pushinteger(s, 0); return 1; } /* __pairs元方法 */ int lua_xshare_pairs(lua_State *s) { /* 自定义pairs元方法的知识点,很少有人提及这个东西,我在这里顺带说一下: lua中的 for key, e in pairs(t) do ... end 是一串语法糖 -- 实际上它等同于下面这样子 local t, next, key = pairs(t);--第一个key必定是nil,就像我们在C里面lua_pushnil(s); while(lua_next(s, n))...一样 while 1 do key, e = next(t, key); if key == nil then break; end--由next返回了key为nil,就说明结束了 ... end */ lua_pushcfunction(s, lua_xshare_next); lua_pushvalue(s, 1); lua_pushnil(s); return 3; } //所有共享数据都存在这里 xshare_table xtabs; int lua_xshare_new(lua_State *s) { std::lock_guard<std::recursive_mutex> lg(xtabs.mtx); if (!lua_gettop(s)) return 0; xshare_key key; if (xshare_get_key(s, 1, key)) return 0; xshare_table *_Result = nullptr; auto it = xtabs.vars.find(key); if (it != xtabs.vars.end()) _Result = std::get<4>(it->second); else { _Result = new xshare_table; xtabs.vars[key] = _Result; } lua_newtable(s); lua_pushstring(s, "__ptr_"); lua_pushlightuserdata(s, _Result); lua_settable(s, -3); lua_getglobal(s, "__xshare_object_metatable"); lua_setmetatable(s, -2); return 1; } int lua_xshare_lock(lua_State *s) { xshare_table *p = xshare_ctab(s); p->mtx.lock(); return 0; } int lua_xshare_unlock(lua_State *s) { xshare_table *p = xshare_ctab(s); p->mtx.unlock(); return 0; } int lua_xshare_mutex(lua_State *s) { xshare_table *p = xshare_ctab(s); p->mtx.lock(); if (lua_gettop(s) < 2 && lua_type(s, 2) != LUA_TFUNCTION) { // 如果是调用xshare.mutex本身的参数错误了,那么就应该先解锁,然后报错 // 因为luaL_error会longjmp到上一个lua_pcall里 p->mtx.unlock(); luaL_error(s, "xshare.mutex args error, should xshare.mutex(share_table, func)"); return 0; } lua_pushvalue(s, 2); lua_pushvalue(s, 1); if (lua_pcall(s, 1, 1, 0)) { printf("xshare_mutex->lua_pcall error:%s\n", lua_tostring(s, -1)); lua_pop(s, 1);//pcall错误时,先输出错误信息,然后将栈弹出 p->mtx.unlock(); return 0; } p->mtx.unlock(); //正常完成之后,返回一个回调函数的返回值 return 1; } void xshare_completeget_push(lua_State *s, xshare_table *p) { for (auto it = p->vars.begin(); it != p->vars.end(); ++it) { xshare_push_key(s, it->first); auto &v = it->second; switch (v.index()) { case 0://std::string lua_pushstring(s, std::get<0>(v).c_str()); break; case 1://intptr_t lua_pushinteger(s, std::get<1>(v)); break; case 2://double lua_pushnumber(s, std::get<2>(v)); break; case 3://xshare_bool(unsigned char) lua_pushboolean(s, std::get<3>(v)); break; case 4://xshare_table* lua_newtable(s); xshare_completeget_push(s, std::get<4>(v)); break; } lua_settable(s, -3); } } int lua_xshare_completeget(lua_State *s) { xshare_table *p = xshare_ctab(s); lua_newtable(s); xshare_completeget_push(s, p); return 1; } int lua_xshare_completeset(lua_State *s) { xshare_table *p = xshare_ctab(s); if (lua_gettop(s) < 2 || lua_type(s, 2) != LUA_TTABLE) luaL_error(s, "xshare.set args error, should xshare.set(share_table, table)"); for (auto it = p->vars.begin(); it != p->vars.end(); ++it) { if (it->second.index() == 4) delete (std::get<4>(it->second)); } p->vars.clear(); xshare_set_tab(s, p, 2); return 0; } int lua_openxsharelib(lua_State *s) { lua_newtable(s); lua_pushcfunction(s, lua_xshare_get); lua_setfield(s, -2, "__index"); lua_pushcfunction(s, lua_xshare_set); lua_setfield(s, -2, "__newindex"); lua_pushcfunction(s, lua_xshare_pairs); lua_setfield(s, -2, "__pairs"); lua_pushcfunction(s, lua_xshart_getn); lua_setfield(s, -2, "__len"); lua_setglobal(s, "__xshare_object_metatable"); lua_newtable(s); lua_pushcfunction(s, lua_xshare_new); lua_setfield(s, -2, "new"); lua_pushcfunction(s, lua_xshare_lock); lua_setfield(s, -2, "lock"); lua_pushcfunction(s, lua_xshare_unlock); lua_setfield(s, -2, "unlock"); lua_pushcfunction(s, lua_xshare_mutex); lua_setfield(s, -2, "mutex"); lua_pushcfunction(s, lua_xshare_completeget); lua_setfield(s, -2, "get"); lua_pushcfunction(s, lua_xshare_completeset); lua_setfield(s, -2, "set"); lua_setglobal(s, "xshare"); return 0; } int lua_closexsharelib() { std::lock_guard<std::recursive_mutex> lg(xtabs.mtx); for (auto it = xtabs.vars.begin(); it != xtabs.vars.end(); ++it) { if (it->second.index() == 4) delete (std::get<4>(it->second)); } xtabs.vars.clear(); }
// ConsoleApplication4.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include "pch.h" #include <thread> #include <iostream> #pragma comment(lib, "lua54.lib") #include "lxsharelib.h" int main(int argc, char **argv) { auto t1 = std::thread([]() { lua_State *s1 = luaL_newstate(); luaL_openlibs(s1); lua_openxsharelib(s1); if (luaL_dofile(s1, "G:\\vs2017\\ConsoleApplication2\\x64\\Release\\script\\xshare.lua")) printf("%s\n", lua_tostring(s1, -1)); lua_close(s1); }); //这里的sleep,是为了让上面那个线程先跑一会儿,因为本例中对共享table的数据写入,是由它完成的。。 std::this_thread::sleep_for(std::chrono::microseconds(10)); //下边这个线程只是测试输出一下,具体可以看lua代码 auto t2 = std::thread([]() { lua_State *s2 = luaL_newstate(); luaL_openlibs(s2); lua_openxsharelib(s2); luaL_dofile(s2, "G:\\vs2017\\ConsoleApplication2\\x64\\Release\\script\\xshare2.lua"); lua_close(s2); }); t1.join(); t2.join(); return 0; }
测试脚本代码:
-- xshare.lua -- print附近的注释为预测输出的内容 local xt = xshare.new(‘test share table‘) xshare.lock(xt); print(‘******************s1******************‘); xt.a = ‘text‘; print(xt.a);--text xt.b = 111222333; print(xt.b);--1122333 xt.c = true; print(xt.c)--true xt.c = false; print(xt.c)--false xt.d = {1,2,3}; --[[ 预期的输出: 1 2 3 ]] for i, e in ipairs(xt.d) do print(e); end xt.d[4] = 4; --[[ 预期的输出: 1 2 3 4 ]] for i, e in ipairs(xt.d) do print(e); end xt.e = {aa=‘1t‘, bb=2, cc=true}; --[[ 要注意:hash表遍历是不能保证顺序的 预期的输出: aa 1t bb 2 cc true ]] for i, e in pairs(xt.e) do print(i, e) end xt.f = {[11]=11,1,2,3,nil,5,6,7,8,9,10,x=12}; print(#(xt.f))-- 11 xt.f[10] = nil; print(#(xt.f))-- 11 xt.f[11] = nil; print(#(xt.f))-- 9 xt.f[9] = nil; print(#(xt.f));-- 8 --[[ 预期的输出: 1:1 x:12 2:2 3:3 5:5 6:6 7:7 8:8 ]] xshare.mutex(xt.f, function(_Tab) for i, e in pairs(_Tab) do print(i..‘:‘..e); end end); -- 使用xshare.mutex主要的目的是避免死锁问题 -- 下面我们来实现一个错误 xshare.mutex(xt, function(_Tab) _Tab.g = function() print(1) end; end); -- 报错之后,继续执行 xshare.mutex(xt, function(_Tab) print(‘test get set‘); -- 完整获取原来的table local old = xshare.get(_Tab); -- 将它设置为一个新的table xshare.set(_Tab, {x = 1, y = 2, z = 3}); --[[ 预测的输出: x:1 y:2 z:3 ]] for i, e in pairs(_Tab) do print(i..‘:‘..e); end -- 还原它 xshare.set(_Tab, old); end) xshare.unlock(xt);
-- xshare2.lua -- print附近的注释为预测输出的内容 local xt = xshare.new(‘test share table‘) xshare.lock(xt); print(‘******************s2******************‘); print(xt.a);--text print(xt.b);--1122333 print(xt.c)--true --[[ 1 2 3 4 ]] for i, e in ipairs(xt.d) do print(e); end --[[ 要注意:hash表遍历是不能保证顺序的 aa 1t bb 2 cc true ]] for i, e in pairs(xt.e) do print(i, e) end xshare.unlock(xt);
******************s1******************
text
111222333
true
false
1
2
3
1
2
3
4
bb 2
cc true
aa 1t
11
11
9
8
1:1
x:12
2:2
3:3
5:5
6:6
7:7
8:8
xshare_mutex->lua_pcall error:G:\vs2017\ConsoleApplication2\x64\Release\script\xshare.lua:90: invalid xshare value type:function
test get set
x:1
z:3
y:2
******************s2******************
text
111222333
false
1
2
3
4
bb 2
cc true
aa 1t