Erlang 随想

接触Erlang不到两个月时间,之前一直用C++开发。Erlang这门语言确实带给我更多的思考。

并发模型

在C++游戏服务器中,我们想要实现一个logger,支持多线程调用。因此该日志系统必须是线程安全的(c++标准输出std::cout不是线程安全的)。对应的主要有两种实现策略:

1.共享资源

常规的思维是,为了实现这个功能,去定义一个接口(函数或类),可提供给所有用户(线程)使用。但由于多个用户共享一个IO设备资源,因此需要在接口内部通过锁或其它同步方式来实现对资源的访问控制。而锁的设计与调试历来是并发程序中最耗费精力的一部分,并且由于代码在调用线程的上下文中执行,因此接口内部发生故障也会影响到调用线程,如死循环,内存越界等。即隔离性差(包括线程之间的隔离,和模块之间的解耦)。

2.消息传递

另一种方式是,将logger抽象为一个单独的执行体,它可以是一个单独的线程,由它来独占IO设备资源,其它线程想要使用IO设备,都需要通知(发送消息)logger线程,由logger线程来操作IO。这样就不会出现资源访问控制的问题。当然你需要自己实现一个线程安全的消息队列,但这毕竟是公共设施,是属于框架层的。NGServerskynet就是这么做的。消息传递方式的瓶颈在于消息拷贝。

再举一个例子,针对于排行榜系统,玩家需要不定期的读取排行榜,而其它玩家的数据变动也会影响到排行榜数据,如果使用共享内存的方式,你可能会使用读写锁,双缓冲,share_ptr copy on write,等多种方式来优化线程之间的同步问题。但仍然是如履薄冰,因为某一次死锁或出错,都可能造成整个游戏服务器宕机。事实上排行榜系统并不是很重要,我们宁愿它无法正常服务,也不应该影响到游戏的正常逻辑。

通过将排行榜独立为一个服务,利用消息传递进行读取于写入,可以很大程度减轻服务之间相互影响的可能性,但往往游戏中的这一类系统太多,一是不能很好地控制和管理这些服务,二是服务之间的消息协议会越来越繁多和复杂,skynet设计者云风也提到了这一点


Erlang

终于主角出场了,Erlang是消息传递的忠实拥护者,并且构建了大量的基础设施来更好支持这一特性。我觉得Erlang的精髓在于面向进程变量不可变语义

1.面向进程

Erlang中的服务就是进程,Erlang中的进程比C++的线程更轻量,可以轻易数万数十万级别。Erlang进程相关的基础设施,包括消息队列,调度算法,消息编码都已经千锤百炼,拿来即用。加之工业级的OTP,消息分发和回调也完成了大半,并且更好地支持了容错和热更等机制。

Erlang中将逻辑抽象成进程,而不是对象,Erlang不支持面向对象。上面提到的日志和排行榜,都可以抽象成一个Erlang进程,让它来负责相关的处理。不需要锁,同时也增加了隔离性,Erlang进程封装了状态

2.变量不可变

Erlang是函数式编程语言,遵循变量不可变语义,一个变量自绑定值起,就一直代表某个值。这种语义刚开始虽然会带来一些不便,但是附带的好处却是巨大的:变量不可变进一步降低了Erlang进程之间相互影响的可能性。例如在NGServer中,你可以通过在线程之间传递指针共享信息,这样做虽通常是为了效率起见,而缺点在于线程之前的关联度增加,并且指针本身的管理和释放也并非易事。在Erlang中,所有的消息都执行拷贝,并且不存在指针引用,因此进程中拿到的消息都是独立的,不变的,这样大幅度降低了犯错的可能性。