这段时间看了一些Go语言相关的东西,发现Go语言的最大特性并发模型类似于C++里面的线程池,正好我们项目服务器也是用的线程池,记录下。
Go语言的并发单位是语言内置的协程,使用关键字go+函数创建一个新的协程,新创建的协程会自动加入到协程调度上下文的等待调度队列,一个协程调度上下文对应一个线程,一个协程调度上下文对应多个协程。新加入的协程会动态负载到各个调度上下文,如果所有调度上下文的平均负载较高时,总调度器会自动创建新的线程和对应的调度上下文用于工作。整体上看,是N个线程:N个调度上下文:M个协程的关系。
我们项目服务器线程架构使用boost::threadpool作为底层,按照配置设定的线程数量启动threadpool,驱动所有Invoker单元,各个Invoker再驱动持有自己的Service运转。单个Service可以持有一个或多个Invoker。在threadpool和Service之间加入Invoker层,逻辑更清晰,实现了Service(父)和ServiceExec(子)的概念。
本质上,Go语言的协程和我们的线程池+Service结构都实现的是线程与逻辑体的N对M映照关系,并且逻辑层完全屏蔽掉线程的概念。差别在于,Go语言的执行体是基于协程,协程切换是用户态切换,而我们的Service间切换是操作系统线程的切换,会有大很多的代价。Go语言是内置支持并发,所以在智能负载这样的细节上也比C++线程池会更强大。经验丰富的C++程序员才能合理驾驭的线程池在Go语言里面就是一个关键字的使用,语言带来的生产力提升真是巨大。
再看线程间通信,Go语言使用内置的通道(chan)类型,我们项目写了一套Service间Message通信,本质上都是基于消息的通信模型。
我们使用的是消息队列轮询机制,每个Service持有一个std::list,Service之间发消息通过ServiceManager中转,Service在心跳中取出list里面的消息并处理,因为是线程间共享变量,所以读取添加消息都加锁。
对于Go语言中带缓存通道,在通道的缓存队列满之前,往通道里面塞数据是非阻塞操作;通道缓存非空的情况下,从通道取数据也是非阻塞操作,这两种情况与我们的Message类似,区别在于Go语言的通道在缓存满时塞数据和缓存空时取数据是阻塞操作,也就是说Go语言的通道带有同步语义,而我们的Message是没有这个功能的。当然,我觉得作为游戏服务器是不怎么需要线程间同步的,基于轮询的Message处理机制已经完全够用。Go语言通道强大在于,每个通道都维护了塞数据协程队列和取数据协程队列,这极大了扩展了通道的能力,真正达到了通道将不同协程连通的目的。
看了Go语言之后,真心觉得用来开发网游服务器实在是太合适了,协程在并发有优势,开发效率会比C++提升不少,而执行效率据说是不会有太大下降,并且语言语法都很和我的胃口。