本文展示了 如何以 C 实现一个通用的“最小堆(Min-Heap)优先队列 并在 Skynet 中通过 Lua C 模块集成使用,从而获得更高的性能与通用性。
一、C 语言模块:cpriorityqueue.c
以下代码演示了一个最小堆的数据结构(以 runTime 作为关键字),并提供了push、pop、peek等接口。该模块以 Lua C API 形式导出函数,让 Lua 层可以通过 require "cpriorityqueue" 来使用。
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdlib.h>
#include <stdio.h>
typedef struct {
double runTime; // 以这个作为排序字段,更通用的话可以改成 sortkey
int id;
// 其他字段也可放在这里,如 priority, pointers to lua references, etc.
} PQItem;
typedef struct {
PQItem *array; // 堆存储数组
int size; // 当前元素数量
int capacity; // 数组容量
} PriorityQueue;
//---------------------------
// 堆操作函数 (C层的私有函数)
//---------------------------
// 上浮操作
static void pq_sift_up(PriorityQueue *pq, int idx) {
while (idx>1) {
int parent = idx/2;
if (pq->array[parent].runTime <= pq->array[idx].runTime)
break;
// swap
PQItem tmp = pq->array[parent];
pq->array[parent] = pq->array[idx];
pq->array[idx] = tmp;
idx = parent;
}
}
// 下沉操作
static void pq_sift_down(PriorityQueue *pq, int idx) {
int n = pq->size;
while (1) {
int left = idx*2;
int right= idx*2+1;
int smallest = idx;
if (left<=n && pq->array[left].runTime < pq->array[smallest].runTime) {
smallest = left;
}
if (right<=n && pq->array[right].runTime < pq->array[smallest].runTime) {
smallest = right;
}
if (smallest==idx) break;
// swap
PQItem tmp = pq->array[idx];
pq->array[idx] = pq->array[smallest];
pq->array[smallest] = tmp;
idx = smallest;
}
}
//---------------------------
// PriorityQueue接口
//---------------------------
static PriorityQueue* pq_create(int cap) {
PriorityQueue* pq = (PriorityQueue*)malloc(sizeof(PriorityQueue));
pq->array = (PQItem*)malloc(sizeof(PQItem)*(cap+1)); // 1-based index
pq->size = 0;
pq->capacity = cap;
return pq;
}
static void pq_free(PriorityQueue *pq) {
if (!pq) return;
if (pq->array) {
free(pq->array);
}
free(pq);
}
static int pq_push(PriorityQueue *pq, double runTime, int id) {
if (pq->size >= pq->capacity) {
// auto expand
int newcap = pq->capacity*2;
PQItem *newarr = (PQItem*)realloc(pq->array, sizeof(PQItem)*(newcap+1));
if (!newarr) return 0; // fail
pq->array = newarr;
pq->capacity = newcap;
}
pq->size++;
int idx = pq->size;
pq->array[idx].runTime = runTime;
pq->array[idx].id = id;
pq_sift_up(pq, idx);
return 1;
}
static int pq_pop(PriorityQueue *pq, double *outVal) {
if (pq->size==0) {
return 0; // empty
}
// root
*outVal = pq->array[1].id;
pq->array[1] = pq->array[pq->size];
pq->size--;
if (pq->size>0) {
pq_sift_down(pq, 1);
}
return 1;
}
static int pq_peek(PriorityQueue *pq, double *outVal) {
if (pq->size==0) {
return 0;
}
*outVal = pq->array[1].id;
return 1;
}
//---------------------------
// Lua 接口绑定
//---------------------------
static int l_pq_create(lua_State* L) {
int cap = luaL_optinteger(L, 1, 16);
PriorityQueue* pq = pq_create(cap);
// userdatum
PriorityQueue** ud = (PriorityQueue**)lua_newuserdata(L, sizeof(PriorityQueue*));
*ud = pq;
// 设置 metatable
luaL_getmetatable(L, "CPriorityQueueMT");
lua_setmetatable(L, -2);
return 1;
}
static int l_pq_gc(lua_State* L) {
PriorityQueue** ud = (PriorityQueue**)luaL_checkudata(L, 1, "CPriorityQueueMT");
if (*ud) {
pq_free(*ud);
*ud = NULL;
}
return 0;
}
static PriorityQueue* checkQueue(lua_State* L) {
PriorityQueue** ud = (PriorityQueue**)luaL_checkudata(L, 1, "CPriorityQueueMT");
luaL_argcheck(L, ud!=NULL && *ud!=NULL, 1, "invalid cpriorityqueue");
return *ud;
}
static int l_pq_push(lua_State* L) {
PriorityQueue* pq = checkQueue(L);
double val = luaL_checknumber(L, 2);
double id = luaL_checknumber(L, 3);
int ret = pq_push(pq, val, id);
lua_pushboolean(L, ret);
return 1;
}
static int l_pq_pop(lua_State* L) {
PriorityQueue* pq = checkQueue(L);
double outVal;
int ret = pq_pop(pq, &outVal);
if (!ret) {
return 0; // return nil
}
lua_pushnumber(L, outVal);
return 1;
}
static int l_pq_peek(lua_State* L) {
PriorityQueue* pq = checkQueue(L);
double outVal;
int ret = pq_peek(pq, &outVal);
if (!ret) {
return 0;
}
lua_pushnumber(L, outVal);
return 1;
}
static int l_pq_size(lua_State* L) {
PriorityQueue* pq = checkQueue(L);
lua_pushinteger(L, pq->size);
return 1;
}
//---------------------------
// 模块入口
//---------------------------
static const struct luaL_Reg pq_methods[] = {
{"push", l_pq_push},
{"pop", l_pq_pop},
{"peek", l_pq_peek},
{"size", l_pq_size},
{NULL, NULL}
};
static const luaL_Reg empty_funcs[] = {
{NULL, NULL}
};
static int l_pq_init(lua_State* L) {
// create metatable
luaL_newmetatable(L, "CPriorityQueueMT");
lua_pushcfunction(L, l_pq_gc);
lua_setfield(L, -2, "__gc");
// methods
lua_newtable(L);
luaL_setfuncs(L, pq_methods, 0);
lua_setfield(L, -2, "__index");
lua_pop(L,1);
// module function
lua_pushcfunction(L, l_pq_create);
lua_setfield(L, -2, "create");
return 1;
}
LUALIB_API int luaopen_cpriorityqueue(lua_State* L) {
luaL_newlib(L, empty_funcs); // new table
l_pq_init(L);
return 1;
}
编译与集成
# 1) 编译器 你也可以改成 gcc
CC = clang
CFLAGS = -O2 -fPIC -Wall
LDFLAGS = -shared -undefined dynamic_lookup
# 2) Lua 头文件包含路径 (示例: ../lua)
INCLUDES = -I ../lua
# 3) 目标名称、源文件
TARGET = cpriorityqueue.so
SOURCES = cpriorityqueue.c
# 4) 默认规则: 编译 .so
all: $(TARGET)
$(TARGET): $(SOURCES)
$(CC) $(CFLAGS) $(INCLUDES) $(LDFLAGS) -o $@ $^
# 如果有更多模块:
# $(TARGET2): $(SOURCES2)
# $(CC) $(CFLAGS) $(INCLUDES) $(LDFLAGS) -o $@ $^
clean:
rm -f $(TARGET)
# rm -f $(TARGET2) ...
- 需根据 Skynet 或 Lua 版本包含路径做相应修改 (依赖于lua,需要链接lua库进行编译)INCLUDES = -I ../lua
- 编译 make clean && make
-
将生成的 cpriorityqueue.so 放在 Skynet 可加载模块目录下(通常 luaclib/ ) 例如:lua_cpath = root .. "luaclib/?.so" 在 skynet 启动配置中 也可以进行配置,确保package.cpath 能搜索到它就可以了
二、在 Skynet lua层中集成流程
Lua层包装 — 之后就可以在lua层直接使用了
-- priority_queue.lua
local cpriorityqueue = require "cpriorityqueue"
local PriorityQueue = {}
PriorityQueue.__index = PriorityQueue
--------------------------------------------------------------------------------
-- Lua层包装
--------------------------------------------------------------------------------
function PriorityQueue:new(initialCap)
local obj = setmetatable({}, self)
-- cObj: C端优先队列指针
obj._cObj = cpriorityqueue.create(initialCap or 16)
obj._nextId = 1
obj._storage = {} -- { [id] = task }
return obj
end
function PriorityQueue:push(task)
-- 1) 确保有 runTime
local runTime = task.runTime
if not runTime then
error("task must have a runTime field!")
end
-- 2) todo 生成id 可以使用唯一 id 生成算法
local id = self._nextId
self._nextId = id + 1
-- 3) 存储到 storage
self._storage[id] = task
-- 4) 调用 C push
local ok = self._cObj:push(runTime, id)
return ok
end
function PriorityQueue:peek()
local id = self._cObj:peek()
if not id then
return nil
end
local t = self._storage[id]
return t
end
function PriorityQueue:pop()
local id = self._cObj:pop()
if not id then
return nil
end
local t = self._storage[id]
if t then
self._storage[id] = nil
return t
end
return nil
end
function PriorityQueue:size()
return cpriorityqueue.size(self._cObj)
end
return PriorityQueue
三、性能评估
-
C语言实现的堆操作复杂度 O(log n) 不变,具体的性能提升(相较于纯 lua 实现来说)还得进一步压测。
四、总结
通过上述方案,就能在Skynet项目中方便地使用C语言的最小堆优先队列。通用性方面:
-
字段扩展:可轻松在 PQItem 中加更多数据(如 id, priority, callbackRef);
-
不止Skynet:只要编译通过并符合Lua C API要求,就可在任何Lua环境中 require。
-
使用:同Lua封装 push/pop/peek/size 即可,类似table操作,但性能更优。
这样,就完成了“以C编写优先队列并集成到Skynet”的详细流程,一些性能热点可以如此实现,以求更好的性能。