在游戏开发中,需要用到大量且更新不频繁的配置数据,而把业务拆分到多个服务后,各个服务可能只用到其中的少部分数据,此时每个服务加载所有数据会浪费大量内存。sharedata模块就是为了解决这种需求设计的,其原理是:将共享lua数据存放到一个c结构里,所有服务都共享这个c结构的内存块,各个服务可以获取这个共享内存对象,然后就可以像读取普通lua表一样读取数据。
调用sharedata.new(name, value) api创建共享数据对象,主要2个参数:name,名字;v,可以是一张lua table,也可以是lua文本代码,也可以是lua文件,最终都会转化成一个table,然后调用c层接口(第15行)返回一个c结构共享内存,共享内存用引用计数管理其生命周期。
-- lualib/skynet/sharedata.lua
function sharedata.new(name, v, ...)
skynet.call(service, "lua", "new", name, v, ...)
end
function CMD.new(name, t, ...)
local dt = type(t)
local value
...
newobj(name, value)
end
local function newobj(name, tbl)
assert(pool[name] == nil)
local cobj = sharedata.host.new(tbl)
sharedata.host.incref(cobj)
local v = { value = tbl , obj = cobj, watch = {} }
objmap[cobj] = v
pool[name] = v
pool_count[name] = { n = 0, threshold = 16 }
end
主要数据结构如下:struct table,是返回给lua层的c结构。
// lualib-src/lua-sharedata.c
struct table { //单个共享对象结构,供lua层操作
int sizearray; //一维数据长度
int sizehash; //hash数据长度
uint8_t *arraytype; //一维数据类型,integer、boolean、string or table
union value * array; //一维数据值
struct node * hash; //hash数据值,是一个数组,每个元素是一个hash数据信息
lua_State * L; //lua虚拟栈,可获取共享的lua table数据
};
struct node { //table里一个key-value值对应的数据信息
union value v;
int key; // integer key or index of string table
int next; // next slot index
uint32_t keyhash;
uint8_t keytype; // key type must be integer or string
uint8_t valuetype; // value type can be number/string/boolean/table
uint8_t nocolliding; // 0 means colliding slot
};
共享数据创建完后,各个服务通过sharedata.query(name)查询共享对象,对象引用计数加一,使用者可以向读取lua表一样读取数据,但其实际上是一个userdata(c结构),所以在corelib里定义了userdata的元表,元表中包含__index、__len、__pairs等方法。比如,__index方法会调用到c层的接口获取指定key对应的value(第5行)。
-- lualib/skynet/sharedata/corelib.lua
local index = core.index
function meta:__index(key)
local obj = getcobj(self)
local v = index(obj, key)
...
return v
end
当需要更新共享对象时,调用sharedata.update(name, v, ...) api,其原理是:创建一个新的共享对象(第17行),然后把旧对象标记为dirty(第20行)。
-- lualib/skynet/sharedata.lua
function sharedata.update(name, v, ...)
skynet.call(service, "lua", "update", name, v, ...)
end
function CMD.update(name, t, ...)
local v = pool[name]
local watch, oldcobj
if v then
watch = v.watch
oldcobj = v.obj
objmap[oldcobj] = true
sharedata.host.decref(oldcobj)
pool[name] = nil
pool_count[name] = nil
end
CMD.new(name, t, ...)
local newobj = pool[name].obj
if watch then
sharedata.host.markdirty(oldcobj)
for _,response in pairs(watch) do
response(true, newobj)
end
end
collect10sec() -- collect in 10 sec
end
此时,已引用该对象的服务并不会马上更新,而是等到下一次使用该共享对象才会判断是否要刷新,即惰性更新。
第4行,如果对象标记为dirty,说明需要更新,调用c库接口进行更新。
-- lualib/skynet/sharedata/corelib.lua
local function getcobj(self)
local obj = self.__obj
if isdirty(obj) then
local newobj, newtbl = needupdate(self.__gcobj)
if newobj then
local newgcobj = newtbl.__gcobj
local root = findroot(self)
update(root, newobj, newgcobj)
if obj == self.__obj then
error ("The key [" .. genkey(self) .. "] doesn't exist after update")
end
obj = self.__obj
end
end
return obj
end
对于旧对象,如果没有服务引用(即引用计数为0时),在下一次collectobj时会删掉它并清理内存(第12行)。
-- service/sharedatad.lua
local function collectobj()
while true do
skynet.sleep(100) -- sleep 1s
if collect_tick <= 0 then
collect_tick = 600 -- reset tick count to 600 sec
collectgarbage()
for obj, v in pairs(objmap) do
if v == true then
if sharedata.host.getref(obj) <= 0 then
objmap[obj] = nil
sharedata.host.delete(obj)
end
end
end
else
collect_tick = collect_tick - 1
end
end
end
调用sharedata.delete(name) 将共享对象的引用计算减一。
本篇文章就写到这,感兴趣的话可以看看我其他关于skynet的文章
在2021年1月13/14号我会开一个四小时玩转skynet训练营,也就是两个礼拜之后,现在已经开放报名,对游戏开发感兴趣的诸位同好可以订阅一下,
训练营内容大概如下:
1. 多核并发编程
2. 消息队列,线程池
3. actor消息调度
4. 网络模块实现
5. 时间轮定时器实现
6. lua/c接口编程
7. skynet编程精要
8. demo演示actor编程思维
期待与诸位同好共襄技术盛举
凭借报名截图可以进群973961276领取上一期skynet训练营的录播以及这期的预习资料哦!