本人表达能力有限,所以文字描述不太清晰,我更习惯自己默默地造*,所以我只能尽力保证我给*可以被直接使用。
虽然不太会说,但有一些前提还是必要讲一下的:
直观的讲: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 = ;}
~xshare_table() {
for (auto it = vars.begin(); it != vars.end(); ++it) {
if (it->second.index() == ) delete (std::get<>(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 -;
}
return ;
} /*
从lua的栈上,获取xshare_table的指针
*/
xshare_table *xshare_ctab(lua_State *s) {
if (!lua_gettop(s) || lua_type(s, ) != LUA_TTABLE) {
luaL_error(s, "invalid share_table");
return nullptr;
} lua_pushstring(s, "__ptr_");
lua_gettable(s, );
if (lua_type(s, -) == LUA_TNIL) {
// 获取 __ptr_失败时,报错
lua_pop(s, );
luaL_error(s, "invalid share_table");
return nullptr;
}
xshare_table *_Result = (xshare_table *)lua_touserdata(s, -);
lua_pop(s, );
return _Result;
} /*
将share_table中的key返回给lua的push环节
*/
void xshare_push_key(lua_State *s, const xshare_key &k) {
switch (k.index()) {
case ://std::string
lua_pushstring(s, std::get<>(k).c_str());
break;
case ://intptr_t
lua_pushinteger(s, std::get<>(k));
break;
case ://double
lua_pushnumber(s, std::get<>(k));
break;
}
} /*
将share_table中的value返回给lua的push环节
*/
void xshare_push_val(lua_State *s, const xshare_value &v) {
switch (v.index()) {
case ://std::string
lua_pushstring(s, std::get<>(v).c_str());
break;
case ://intptr_t
lua_pushinteger(s, std::get<>(v));
break;
case ://double
lua_pushnumber(s, std::get<>(v));
break;
case ://xshare_bool(unsigned char)
lua_pushboolean(s, std::get<>(v));
break;
case ://xshare_table*
lua_newtable(s);
lua_pushstring(s, "__ptr_");
lua_pushlightuserdata(s, std::get<>(v));
lua_settable(s, -);
lua_getglobal(s, "__xshare_object_metatable");
lua_setmetatable(s, -);
break;
}
} /*
__index元方法
*/
int lua_xshare_get(lua_State *s) {
auto p = xshare_ctab(s);
xshare_key k;
if(xshare_get_key(s, , k))
luaL_error(s, "invalid xshare_key type:%s", lua_typename(s, lua_type(s, ))); auto it = p->vars.find(k);
if (it == p->vars.end())
return ;
xshare_push_val(s, it->second);
return ;
} 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() == )
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<>(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<>(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<>(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<>(it->second);
}
}
else {
_rt = new xshare_table;
t->vars[key] = _rt;
} return xshare_set_tab(s, _rt, ((n > ) ? n : -));//递归前进, n比0大时,直接传入n,否则直接传入-2(它包含了lua_pushnil,所以是-2)
break;
}
default:
return -;
} return ;
} int xshare_set_tab(lua_State *s, xshare_table *t, int n) {
lua_pushnil(s);
int _Result = ;
while (lua_next(s, n)) {
xshare_key key;
if (xshare_get_key(s, -, key)) {
//获取key错误时,清理栈并返回错误信息
_Result = ( << ) | lua_type(s, -);
lua_pop(s, );
break;
} if (key.index() == ) {
//整数key时,记录最大index
intptr_t i = std::get<>(key);
if (i > t->imax)
t->imax = i;
} int rc = xshare_set_tabval(s, t, key, -); // 由此处判断rc的理由是,在产生错误之后,让最上层的调用处去报告错误,这样就能在出错之后最起码保证lua栈还是正确的
if (rc == -) {
//设置数据错误时,如果返回值为-1,说明错误在当前这一层table里,此时应该清理栈并返回错误信息
_Result = ( << ) | lua_type(s, -);
lua_pop(s, );
break;
}
else {
//如果返回值不为0,说明错误在由更下层的table返回,此时应该直接清理栈并返回rc
if (rc != ) {
_Result = rc;
lua_pop(s, );
break;
}
} lua_pop(s, );
} return ;
} /*
__newindex元方法
*/
int lua_xshare_set(lua_State *s)
{
auto p = xshare_ctab(s);
xshare_key key;
if(xshare_get_key(s, , key))
luaL_error(s, "invalid xshare_key type:%s", lua_typename(s, lua_type(s, ))); int vt = lua_type(s, );
auto it = p->vars.find(key);
if (it != p->vars.end()) {
if (key.index() == ) {
intptr_t ikey = std::get<>(it->first);
if (vt == LUA_TNIL) {
// 删除了最大的一个整数key时,让imax-1
if (p->imax == ikey)
p->imax--; if (it->second.index() == ) //如果被设置为nil的对象是一个xshare_table,删除它
delete (std::get<>(it->second));
p->vars.erase(it);
return ;
} //被设置的ikey比imax更大时,更改记录
if (ikey > p->imax)
p->imax = ikey;
}
else
{
if (vt == LUA_TNIL) {
if (it->second.index() == ) //如果被设置为nil的对象是一个xshare_table,删除它
delete (std::get<>(it->second));
p->vars.erase(it);
return ;
}
}
}
else
{
// 当key不存在时,如果value是nil,直接返回
// lua本身这个样子操作好像是要报错的,但这个报错我感觉意义并不大,也许这仅仅是我的感觉,如果要让它报错,删掉这个判断即可
if (vt == LUA_TNIL) return ;
if (key.index() == ) {
intptr_t ikey = std::get<>(key);
if (ikey > p->imax)
p->imax = ikey;
}
} int rc = xshare_set_tabval(s, p, key, );
if (rc == -) {
luaL_error(s, "invalid xshare value type:%s", lua_typename(s, lua_type(s, )));
}
else {
if (rc != ) {
luaL_error(s,
(((((unsigned int)rc) >> ) & 0xFFFF) == ) ? "invalid xshare_key type:%s" : "invalid xshare value type:%s",
lua_typename(s, (((unsigned int)rc) & 0xFFFF)));
}
}
return ;
} /*
给迭代器返回的next函数
*/
int lua_xshare_next(lua_State *s) {
auto p = xshare_ctab(s);
auto it = p->vars.end(); if (lua_gettop(s) > && lua_type(s, ) != LUA_TNIL) {
xshare_key key;
if(xshare_get_key(s, , key))
luaL_error(s, "invalid xshare_key type:%s", lua_typename(s, lua_type(s, )));
it = p->vars.find(key);
} if (it != p->vars.end())
++it;
else
it = p->vars.begin(); if (it == p->vars.end())
return ;
xshare_push_key(s, it->first);
xshare_push_val(s, it->second);
return ;
} /*
__len元方法,也就是#tab
*/
int lua_xshart_getn(lua_State *s) {
auto p = xshare_ctab(s);
intptr_t j = p->imax;
if (j > ) {
auto it = p->vars.find(j);
if (it != p->vars.end())
{
lua_pushinteger(s, j);
return ;
} //这一段二叉查找整数key边界,我是直接从lua5.3中的ltable.c中luaH_getn里抄过来的,
//在5.4中它又有了新的变化,预先存在了一系列路径判断,尽可能的考虑不去用哈希表搜索,但最坏的情况还是存在这段代码,在binsearch函数中
intptr_t i = ;
while (j - i > ) {
intptr_t m = (i + j) >> ;
if (p->vars.find(m) == p->vars.end())
j = m;
else
i = m;
} p->imax = i;
lua_pushinteger(s, i);
return ;
}
lua_pushinteger(s, );
return ;
} /*
__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, );
lua_pushnil(s);
return ;
} //所有共享数据都存在这里
xshare_table xtabs; int lua_xshare_new(lua_State *s) {
std::lock_guard<std::recursive_mutex> lg(xtabs.mtx);
if (!lua_gettop(s))
return ;
xshare_key key;
if (xshare_get_key(s, , key))
return ;
xshare_table *_Result = nullptr;
auto it = xtabs.vars.find(key);
if (it != xtabs.vars.end())
_Result = std::get<>(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, -);
lua_getglobal(s, "__xshare_object_metatable");
lua_setmetatable(s, -);
return ;
} int lua_xshare_lock(lua_State *s) {
xshare_table *p = xshare_ctab(s);
p->mtx.lock();
return ;
} int lua_xshare_unlock(lua_State *s) {
xshare_table *p = xshare_ctab(s);
p->mtx.unlock();
return ;
} int lua_xshare_mutex(lua_State *s) {
xshare_table *p = xshare_ctab(s);
p->mtx.lock();
if (lua_gettop(s) < && lua_type(s, ) != 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 ;
} lua_pushvalue(s, );
lua_pushvalue(s, );
if (lua_pcall(s, , , )) {
printf("xshare_mutex->lua_pcall error:%s\n", lua_tostring(s, -));
lua_pop(s, );//pcall错误时,先输出错误信息,然后将栈弹出
p->mtx.unlock();
return ;
} p->mtx.unlock();
//正常完成之后,返回一个回调函数的返回值
return ;
} 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 ://std::string
lua_pushstring(s, std::get<>(v).c_str());
break;
case ://intptr_t
lua_pushinteger(s, std::get<>(v));
break;
case ://double
lua_pushnumber(s, std::get<>(v));
break;
case ://xshare_bool(unsigned char)
lua_pushboolean(s, std::get<>(v));
break;
case ://xshare_table*
lua_newtable(s);
xshare_completeget_push(s, std::get<>(v));
break;
}
lua_settable(s, -);
}
} int lua_xshare_completeget(lua_State *s) {
xshare_table *p = xshare_ctab(s);
lua_newtable(s);
xshare_completeget_push(s, p);
return ;
} int lua_xshare_completeset(lua_State *s) {
xshare_table *p = xshare_ctab(s);
if (lua_gettop(s) < || lua_type(s, ) != 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() == )
delete (std::get<>(it->second));
}
p->vars.clear();
xshare_set_tab(s, p, );
return ;
} int lua_openxsharelib(lua_State *s) {
lua_newtable(s);
lua_pushcfunction(s, lua_xshare_get);
lua_setfield(s, -, "__index"); lua_pushcfunction(s, lua_xshare_set);
lua_setfield(s, -, "__newindex"); lua_pushcfunction(s, lua_xshare_pairs);
lua_setfield(s, -, "__pairs"); lua_pushcfunction(s, lua_xshart_getn);
lua_setfield(s, -, "__len"); lua_setglobal(s, "__xshare_object_metatable"); lua_newtable(s); lua_pushcfunction(s, lua_xshare_new);
lua_setfield(s, -, "new"); lua_pushcfunction(s, lua_xshare_lock);
lua_setfield(s, -, "lock"); lua_pushcfunction(s, lua_xshare_unlock);
lua_setfield(s, -, "unlock"); lua_pushcfunction(s, lua_xshare_mutex);
lua_setfield(s, -, "mutex"); lua_pushcfunction(s, lua_xshare_completeget);
lua_setfield(s, -, "get"); lua_pushcfunction(s, lua_xshare_completeset);
lua_setfield(s, -, "set"); lua_setglobal(s, "xshare"); return ;
} 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() == )
delete (std::get<>(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, -));
lua_close(s1);
}); //这里的sleep,是为了让上面那个线程先跑一会儿,因为本例中对共享table的数据写入,是由它完成的。。
std::this_thread::sleep_for(std::chrono::microseconds()); //下边这个线程只是测试输出一下,具体可以看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 ;
}
测试脚本代码:
-- xshare.lua
-- print附近的注释为预测输出的内容
local xt = xshare.new('test share table') xshare.lock(xt);
print('******************s1******************');
xt.a = 'text';
print(xt.a);--text
xt.b = ;
print(xt.b);--
xt.c = true;
print(xt.c)--true
xt.c = false;
print(xt.c)--false xt.d = {,,}; --[[
预期的输出:
1
2
3
]]
for i, e in ipairs(xt.d) do
print(e);
end xt.d[] = ; --[[
预期的输出:
1
2
3
4
]]
for i, e in ipairs(xt.d) do
print(e);
end xt.e = {aa='1t', bb=, cc=true}; --[[ 要注意:hash表遍历是不能保证顺序的
预期的输出:
aa 1t
bb 2
cc true
]]
for i, e in pairs(xt.e) do
print(i, e)
end xt.f = {[]=,,,,nil,,,,,,,x=}; print(#(xt.f))--
xt.f[] = nil;
print(#(xt.f))--
xt.f[] = nil;
print(#(xt.f))--
xt.f[] = nil;
print(#(xt.f));-- --[[
预期的输出:
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() end;
end); -- 报错之后,继续执行 xshare.mutex(xt, function(_Tab) print('test get set');
-- 完整获取原来的table
local old = xshare.get(_Tab); -- 将它设置为一个新的table
xshare.set(_Tab, {x = , y = , z = }); --[[
预测的输出:
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);-- 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