一. 定义

通常意义上,在C++中,可取地址,有名字的即为左值。不可取地址,没有名字的为右值。右值主要包括字面量,函数返回的临时变量值,表达式临时值等。右值引用即为对右值进行引用的类型,在C++98中的引用称为左值引用。

如有以下类和函数:

1
2
3
4
5
6
7
8
9
10
11
12

class A
{
private:
int* _p;
};

A ReturnValue()
{
return A();
}

ReturnValue()的返回值即为右值,它是一个不具名的临时变量。在C++98中,只有常量左值引用才能引用这个值。

1
2
3
4
5

A& a = ReturnValue(); // error: non-const lvalue reference to type 'A' cannot bind to a temporary of type 'A'

const A& a2 = ReturnValue(); // ok

通过常量左值引用,可以延长ReturnValue()返回值的生命周期,但是不能修改它。C++11的右值引用出场了:

A&& a3 = ReturnValue();

右值引用通过”&&”来声明, a3引用了ReturnValue()的返回值,延长了它的生命周期,并且可以对该临时值进行修改。

阅读全文 »

skynet提供一个gateserver用于处理网络事件,位于lualib/snax/gateserver.lua。云风在skynet wiki上介绍了gateserver的功能和使用范例。用户可以通过向gateserver提供一个自定义handle来向gateserver注册事件处理(玩家登录,断开,数据到达等)。

gateserver模块使用C编写的socketdriver和netpack模块,gateserver被加载时,会注册对”socket”(PTYPE_SOCKET)类型消息的处理,并且通过netpack.filter对收到的数据进行分包。分包完成后调用传入的handler对应的处理函数进行处理。它替上层使用者,完成对PTYPE_SOCKET消息的注册分发,以及消息分包。这样在使用时,只需提供一个handler,然后调用gateserver.start(handler)即可。

在skynet中,如果你要自定义你的gate网关服务gate.lua,需要执行以下几步:

  1. gateserver = require snax.gateserver
  2. gateserver.start(handler)向gateserver注册网络事件处理。
  3. skynet.call(gate, "lua", "open", conf)在外部向你定义的gate服务发送启动消息,并传入启动配置(端口,最大连接数等)来启动gate服务。
阅读全文 »

1. 异步IO

skynet用C编写的sokcet模块使用异步回调机制,通过lualib-src/lua-socket.c导出为socketdriver模块。skynet socket C API使用的异步回调方式是:在启动socket时,记录当前服务handle,之后该socket上面的消息(底层使用epoll机制)通过skynet消息的方式发往该服务。这里的当前服务指的是socket启动时所在的服务,对于被请求方来说,为调用socketdriver.start(id)的服务,对于请求方来说,为调用socketdriver.connect(addr,port)的服务。skynet不使用套接字fd在上层传播,因为在某些系统上fd的复用会导致上层遇到麻烦,skynet socket C API为每个fd分配一个ID,是自增不重复的。

阅读全文 »

前段时间关注到disruptor,一个高并发框架。能够在无锁(lock-free)的情况下处理多生产者消费者的并发问题。它可以看作一个消息队列,通过CAS而不是锁来处理并发。

因此实现了一个C++版本的disruptor,基于ring buffer,实现一个发送缓冲(多生产者,单消费者)。

写入缓冲

某个生产者要写入数据时,先申请所需空间(需要共享当前分配位置),然后直接执行写入,最后提交写入结果(需要共享当前写入位置)。整个写入过程由两个关键共享变量: atomic_ullong _alloc_countatomic_ullong _write_count。前者负责管理和同步当前分配的空间,后者负责同步当前已经写入的空间。也就是说,整个过程分为三步:申请,写入,提交。

比如,有两个生产者P1和P2。P1申请到大小为50的空间,假设此时_alloc_count=10,那么P1将得到可写入位置10,此时_alloc_count更新为60。P1此时可以执行写入(无需上锁)。这个时候P2开始申请大小为10的空间,它将得到写入位置60,_alloc_count更新为70。因此实际上P1和P2是可以并发写的。如果P2比P1先写完,它会尝试提交,此时由于P1还没有提交它的写入结果,因此P2会自旋等待(不断尝试CAS操作)。直到P1提交写入结果后,P2才能提交。通过CAS可以保证这种提交顺序。提交操作会更新_write_count变量,提交之后的数据便可以被消费者读取使用。

上面的描述并没有提到缓冲区不够的问题,为了判断缓冲区当前可写空间,还需要一个变量 atomic_ullong _idle_count用于记录当前缓冲区空闲大小。该变量在生产者申请空间后减小,在消费者使用数据后变大。初始等于整个ring buffer的大小。

阅读全文 »

协程(协同式多线程)是一种用户级的非抢占式线程。用户级是指它的切换和调度由用户控制,非抢占指一个协程只有在其挂起(yield)或者协程结束才会返回。协程和C线程一样,有自己的堆栈,自己的局部变量,自己的指令指针,并且和其它协程共享全局变量等信息。用户可以实现自己调度协程,这主要得益于yield函数可以自动保存协程当前上下文,这样当挂起的协程被唤醒(resume)时,会从yield处继续向下执行,看起来就像是一个”可以返回多次的函数”。协程还有一个强大的功能就是可通过resume/yield来交换数据,这样使得它可以用于异步回调:当执行异步代码时,切换协程,执行完成后,再切换回来(附带异步执行结果)。由于切换都是用户控制的,在同一时刻只有一个协同程序在运行(这也是和传统线程最大的区别之一),因此无需考虑同步和加锁的问题。

阅读全文 »

在多核的CPU架构中,每一个核心core都会有自己的缓存行(cache line),因此如果一个变量如果同时存在不同的核心的cache line时,就会出现伪共享(false sharing)的问题。此时如果一个核心修改了该变量,该修改需要同步到其它核心的缓存。

上图说明了伪共享的问题。在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核心2获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。

我们可以通过padding来确保两个共享变量不位于同一个cache-line中,这对于链表等传统结构的共享(首尾节点通常位于同一cache-line)有重大意义。如下面这个例子:

阅读全文 »

C模块的导出

从skynet核心模块来看,它只认得C服务,每个服务被编译为动态库,在需要时由skynet加载。skynet提供发送消息和注册回调函数的接口,并保证消息的正确到达,并调用目标服务回调函数。其它东西,如消息调度,线程池等,对于用户来说都是透明的。

skynet服务可以由lua编写,因此skynet将C模块核心接口通过skynet/lualib-src/lua-skynet.c导出为 skynet.so提供给lua使用。在lua层,通过skynet/lualib/skynet.lua加载C模块(require "skynet.core")完成对C API的封装。主要涉及lua服务的加载和退出,消息的发送,回调函数的注册等。用户定义的lua服务通过require "skynet"的接口即可完成服务的注册,启动和退出等。关于skynet lua api可以参见skynet wiki

阅读全文 »

这些天一直在拜读云风的skynet,由于对lua不是很熟悉,也花了一些时间来学习lua。这里大概整理一下这些天学习skynet框架的一些东西。

skynet核心概念为服务,一个服务可以由C或lua实现,服务之间的通信已由底层C框架保证。用户要做的只是注册服务,处理消息。如云风的skynet综述中所说:

作为核心功能,Skynet 仅解决一个问题:

把一个符合规范的 C 模块,从动态库(so 文件)中启动起来,绑定一个永不重复(即使模块退出)的数字 id 做为其 handle 。模块被称为服务(Service),服务间可以自由发送消息。每个模块可以向 Skynet 框架注册一个 callback 函数,用来接收发给它的消息。每个服务都是被一个个消息包驱动,当没有包到来的时候,它们就会处于挂起状态,对 CPU 资源零消耗。如果需要自主逻辑,则可以利用 Skynet 系统提供的 timeout 消息,定期触发。

Skynet 提供了名字服务,还可以给特定的服务起一个易读的名字,而不是用 id 来指代它。id 和运行时态相关,无法保证每次启动服务,都有一致的 id ,但名字可以。

阅读全文 »

lua和C交互的核心就是lua栈,lua和C的所有数据交互都是通过lua栈来完成的。

一. C调用lua

C调用lua很简单,通常C以lua作为配置脚本,在运行时读取脚本数据,主要步骤:

  1. 加载脚本 luaL_loadfile
  2. 运行脚本 lua_pcall
  3. 获取数据 lua_getglobal ….
  4. 使用数据 lua_tostring lua_pcall …

二. 在lua脚本中调用C:

在C程序中,使用lua作为脚本,但是要在运行脚本时,访问C中定义的一些变量或函数。

  1. 将C变量或函数(遵从指定函数原型,见下面三 Step 1)push到lua栈中
  2. 通过lua_setglobal为当前lua栈顶的函数或变量命名,这样在lua中可通过该名字完成对变量或函数的使用
  3. 之后可在加载的lua脚本中使用C变量或函数
阅读全文 »

PlayerSession类

在之前的网络底层设计中,Player和Session之间通过组合实现弱关联,但仍然有个诟病:Player类和Session类在网络连接到来时一并创建了。这样后面在做断线重连的时候,会有两个Player。而事实上LoginService只管登录认证,登录认证的时候并不需要创建Player类,因此可以延迟Player的创建,将其放在MapService中。而这之前LoginService的登录认证也需要用户的一些基本信息。基于这些,实现了PlayerSession类:

阅读全文 »