C++ 多线程调用 Lua 的正确姿势

目录 C++, Lua, 服务端

上一篇文章《轻量级的 C++ Lua 传参方法 - Protobuf 反射》提到我正在 C++ 项目中使用简单的 Lua 脚本做一些灵活的程序逻辑,这样可以把多个 Lua 脚本放在数据库或者缓存里,根据不同的条件选择执行不同的脚本,而且可以在线更新这些脚本。

但在 C++ 项目中集成 Lua,遇到的第一个问题就是多线程问题。Lua 本身是不知晓宿主程序线程环境的,所以 lua_State 的多线程访问是不安全的。而多线程又是服务端程序的天然特性,我暂时还不想把所有逻辑都托管给 Lua,用它自己的多线程机制。所以看起来有下面几个选择:

一是把 Lua 脚本也看成是一个独立服务,通过 RPC 或者消息队列的方式去调用它,在 Lua 脚本内部处理性能问题。这样做相当于用 Lua 写了个服务,脚本复杂度比较高,偏离了我的本意。

二是在每个 C++ 线程里,都创建一个独立的 Lua VM。每个线程有自己的上下文,也自然就互不干扰了。这样做会浪费一些内存,但考虑到 Lua VM 几十 K 级别的大小,对服务端来说根本不算什么开销。但创建线程级的 Lua VM,内存管理上又有不同的方法。

__thread 修饰符只能约束 POD 变量,的确可以存 lua_State 指针,可是它不支持指针的销毁,必须自己额外管理 lua_State 对象,然后再把指针传给线程变量。thread_local 的确能支持复杂类型,可以在析构里销毁 lua_State,可又要求 C++11。思来想去,用pthread_key_create/delete + pthread_get/setspecific 是一个相对稳妥又较为简单的方法,即不用额外自动管理内存,又能实现在线程结束后自动析构线程自己的 Lua VM。

三是将 C++ 的线程与 Lua 的线程对应起来,使用同一个 Lua VM,但在每个 C++ 线程中都用lua_newthread 创建一个 Lua 线程 State 指针。不过在创建线程那一刻,仍然需要对主 lua_State 加锁。这其实相当于给每个 C++ 线程都创建了一个独立的 Lua 堆栈,这样在传参和执行脚本的时候就不担心有数据冲突。理论上来讲效果应该与二是类似的。

四是使用一个线程安全的对象池。将 lua_State 指针放到对象池里,需要的时候拿出来,用完再放回去,由对象池来管理创建和销毁。这就需要一个额外的内存管理容器,代码量大一些,只是对很多成熟的产品来说,可能本身就有这样的轮子。

考虑到代码量,复杂度等问题,我实际在项目中采取了二方案。不过我对 Lua 的了解还不深入,不知道是否还有更好的办法?

一种轻量级 C++ Lua 传参方法 - Protobuf 反射

目录 C++, Lua, 服务端

虽然很多动态语言(例如 PHP)的性能在近些年有了大幅度的提升,也得到了更广泛的应用,但是在一些对性能要求比较严苛的场合,C/C++ 还是有着难以替代的优势。可 C/C++ 最大的缺点就是它的不够灵活,很小一点修改都必须得重新编译,部署,重启上线。为了增强 C/C++ 的灵活性,很多项目都选择嵌入 Lua 解析器来处理程序逻辑中的动态部分,我们也不例外。

目前我们对 Lua 的使用还是比较保守,主要是封装了一些基于特定条件的排序或者过滤规则。它的特点就是传入参数较多,但返回值特别少,基本上就是一个数字或者布尔值。最开始是使用的原始方法,手工去拼 Lua Table 作为传入参数,每加一个参数,就要手写几行添加元素的代码。最近我看到 brpc 里的 pb2json ,忽然想到完全可以用 Protobuf 的反射机制,自动拼 Lua Table。下面是基本类型的转换方法,当然,也可以用类似的方法对 Protobuf 的 map, message 等高级数据结构进行进一步封装。

void ProtoMessageToLuaTable(const google::protobuf::Message &message, lua_State *L) {
    lua_newtable(L);
    const Descriptor* descriptor = message.GetDescriptor();
    const Reflection* reflection = message.GetReflection();
    int field_count = descriptor->field_count();
    for (int i = 0; i < field_count; ++i) {
        const FieldDescriptor* field = descriptor->field(i);
        switch (field->type()) {
        case FieldDescriptor::TYPE_BOOL:
            lua_pushboolean(L, reflection->GetBool(message, field));
            break;
        case FieldDescriptor::TYPE_UINT32:
            lua_pushinteger(L, reflection->GetUInt32(message, field));
            break;
        case FieldDescriptor::TYPE_UINT64:
            lua_pushinteger(L, reflection->GetUInt64(message, field));
            break;
        case FieldDescriptor::TYPE_INT32:
        case FieldDescriptor::TYPE_SINT32:
            lua_pushinteger(L, reflection->GetInt32(message, field));
            break;
        case FieldDescriptor::TYPE_INT64:
        case FieldDescriptor::TYPE_SINT64:
            lua_pushinteger(L, reflection->GetInt64(message, field));
            break;
        case FieldDescriptor::TYPE_FLOAT:
            lua_pushnumber(L, static_cast<double>(reflection->GetFloat(message, field)));
            break;
        case FieldDescriptor::TYPE_DOUBLE:
            lua_pushnumber(L, reflection->GetDouble(message, field));
            break;
        case FieldDescriptor::TYPE_STRING:
            lua_pushstring(L, reflection->GetString(message, field).c_str());
            break;
        default:
            lua_pushnil(L);
            break;
        }
        lua_setfield(L, -2, field->name().c_str());
    }
}

其实调研了一下,发现还有一些其它的方法,比如 luabind, sol2一堆库。但这些工具更适合 C++ Lua 交互比较复杂的场合,而且也引入了额外的依赖和额外的要求(比如 C++11)。对于像我们这样的简单场景,在不引入更多依赖的情况下使用 Protobuf 反射机制,不失为一个好的选择。

合并 Debian 补丁的 OpenBSD netcat Linux 源码

目录 C++, 开源

前几天在我的 CentOS 4.3 古董服务器上想使用 ProxyCommand 给 ssh 配置 socks 代理,ssh -o "ProxyCommand nc -X 5 -x 127.0.0.1:1080 %h %p" 选项在我的 OSX/Ubuntu 上挺好用的,但是在 CentOS 4.3 却发现 " invalid option -- x",没有这个参数。

本来我以为是 netcat 没有更新到最新,特地去下载了 GNU netcat 最新的源码包,结果源码编译后还是没有 “-x” 这个参数。后来仔细看 man page,看起来根本就不是一个版本。调研了下,才发现 netcat 居然有好多的版本:

你们城里人可真会玩儿啊!

因为 OSX/Ubuntu 都是用的 OpenBSD netcat(移植或修改版),所以 -x/X 参数是存在的,能够实现代理功能;但是 CentOS 4 因为版本太老,用的还是 GNU netcat。本来找个移植后的源码包,直接编译安装就好了呗。但可是,我 Google 了半天 (打脸,谁让你还用 CentOS 4),还是没找到能直接编译的 OpenBSD netcat Linux 源码包,最后还是在 Arch 的某个网站上找到使用 Debian 源码进行 Patch 然后再编译的脚本,才搞明白怎么能在 Linux 编出来 OpenBSD netcat 。

可能是出于易维护的考虑吧,Debian 把源码分成了两个包,一个是原始的 OpenBSD netcat,一个是 Debian 的 N 个 Patch 源码。编译时要先把 Patch 打到 OpenBSD 源码上,然后再编译。可这样的过程不是维护者很难理解,为什么不多发布个打完 Patch 的源码呢?而且这种补丁包形式也没个官方网站介绍下,真的好难懂。

为了避免其它古董 Linux 发行版用户再有我这样的苦恼,我把 patch 后的代码上传到了 Github: https://github.com/solrex/netcat ,有需要的朋友可以自取。