协程(协同式多线程)是一种用户级的非抢占式线程。用户级是指它的切换和调度由用户控制,非抢占指一个协程只有在其挂起(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类:

阅读全文 »

总结下几个使用shared_ptr需要注意的问题:

一. 相互引用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class C;
class B : public std::enable_shared_from_this<B>
{
public:
~B(){ cout << "~B" << endl; }
void SetPC(std::shared_ptr<C>& pc){ _pc = pc; }

private:
std::shared_ptr<C> _pc;
};

class C : public std::enable_shared_from_this<C>
{
public:
~C(){ cout << "~C" << endl; }
void SetPB(std::shared_ptr<B>& pb){ _pb = pb; }

private:
std::shared_ptr<B> _pb;
};

int main()
{
std::shared_ptr<C> pc = std::make_shared<C>();
std::shared_ptr<B> pb = std::make_shared<B>();
pc->SetPB(pb);
pb->SetPC(pc);
return 0;
}

上面的代码中,B和C均不能正确析构,正确的做法是,在B和C的释放函数,如Close中,将其包含的shared_ptr置空。这样才能解开引用链。

阅读全文 »

本文是《深度探索C++对象模型》的读书笔记,主要根据自己的理解整理一下C++对象构造的实现细节以及其在实际编程中所带来的影响。

一. 构造函数

C++95标准对构造函数如下描述:

对于 class X,如果没有任何user-declared constructor,那么会有一个default constructor被隐式声明出来…..一个被隐式声明出来的 default constructor 将是一个trivial(浅薄无能的,没啥用的) constructor ……

上面的话摘自《深度探索C++对象模型》P40,由于其省略了其中c++标准的部分内容,因此很容易造成误解:

编译器隐式生成的构造函数都是 trivial constructor …..

事实上,描述中提到 default constructor 被隐式声明出来(满足语法需要),而该构造函数是否被编译器合成(实现或定义),取决于编译器是否需要在构造函数中做些额外工作,一个没有被合成的 default constructor 被视为 trivial constructor(这也是c++标准原话的意思),而当编译器在需要时合成了构造函数,那么该类构造函数将被视为 nontrivial。

另外,一个定义了 user-decalred constructor(用户定义的任何构造函数) 的类被视为具有 nontrivial constructor。

下面将着重讨论编译器隐式声明的构造函数在哪种情况下需要被合成(nontrivial),哪种情况下无需被合成(trivial):

阅读全文 »

消息编解码(或序列化)主要是将消息体由一些标准库容器或自定义的类型,转化成二进制流,方便网络传输。为了减少网络IO,编解码中也可能在存在数据”压缩和解压”,但这种压缩是针对于特定的数据类型,并不是针对于二进制流的。在NGServer的消息编解码中,并不涉及数据压缩。

一. 消息编码格式

NGServer的消息分为首部和消息体,首部共四个字节,包括消息长度(包括首部)和消息ID,各占两个字节。消息体为消息编码后的二进制数据。

在消息体中,针对于不同的数据类型而不同编码。对于POD类型,直接进行内存拷贝,对于非POD类型,如标准库容器,则需要自定义编码格式,以下是几种最常见的数据类型编码:

std::string 先写入字符串长度,占两个字节,再写入字符串内容。
std::vector 先写入vector的元素个数(占两个字节),在对其元素逐个递归编码(如果元素类型为string,则使用string的编码方式)。
std::list 编码方式与vector类似
T arr[N] 对于这种类型,不需要写入元素个数,因为在消息结构体中指出了固定长度N,因此可以通过模板推导得到N。所以递归写入N个元素T即可。对于简单数据类型T,如T为char时,可以通过模板特例化对其优化。

阅读全文 »