大型网络游戏服务器的逻辑大多采用单线程设计,典型的就是一个线程处理一个区域(地图),跨区域通过跳转实现,这样,不同区域的对象在逻辑上是不发生交互的。
这样在一台服务器上开启N个线程就可以处理N个区域。但一个线程处理一个区域毕竟有其瓶颈,如果一个区域内挤进了过多的玩家就会导致为那个区域服务的线程
不负重和,表现就是那个区域中的玩家发现操作响应变得不及时.
最近一段时间在思考如何能并行的利用多进程多机器为同一片区域服务,这样可以通过多开几个进程提升单区域的承载量。一种方式是类似bigworld全分布式设计,
同一区域中的对象可以分布在N个进程/机器中。以一个玩家为例,在一个进程中其是主动对象,所有对这个玩家对象的属性操作都必须在主动对象上执行.在其它进
程中的对象要跟这个玩家交互,例如战斗,所以这个玩家对象必须在其主动对象所在进程以外有副本对象,这样在另外一个进程中的对象可以根据这个玩家的副本
对象的属性值计算战斗伤害,抵抗等等,然后将属性变更请求发回给玩家所在的主动对象,执行实际的属性变更.以这种方式,主动对象的属性变更必须通知到所有
的副本对象。由于主动对象的属性变更后需要通知所有的副本对象,当这个区域中对象数量变得很大时,属性变更带来的通信量将会非常大.
现在换个思路,结合多线程和多进程,在一个进程中启动多个线程,主动对象位于一个线程中,属性数据在多个线程中可见,只有主动对象所在线程能修改那个对象
的属性,其它线程只能读取,这样,同一个进程中的多个线程就免除了属性同步的需要.
对于32/64位的基本属性,其读写本身就是原子的,所以可以安全的实现一个线程写,N个线程读.但对于一些结构型的属性,最典型的就是坐标,由x,y,z三个分量
构成,对其读/写都不是原子的.为了实现原子的读写坐标,最简单的做法就是在SetPos/GetPos中加锁。但再想一想,我们要读取的只是一份完整,正确的坐标数据,
却可以容忍其不一定是最新的数据。所以,下面实现了一个无锁的算法实现安全的对结构体的1写N读。
#include "atomic.h" volatile int get_count; volatile int set_count; volatile int miss_count; struct atomic_st { volatile int32_t version; char data[]; }; struct atomic_type { uint32_t g_version; int32_t index; volatile struct atomic_st *ptr; int32_t data_size; struct atomic_st* array[2]; }; struct atomic_type *create_atomic_type(uint32_t size); void destroy_atomic_type(struct atomic_type **_at); #define GET_ATOMIC_ST(NAME,TYPE)\ TYPE NAME(struct atomic_type *at)\ {\ TYPE ret;\ while(1)\ {\ struct atomic_st *ptr_p = (struct atomic_st *)at->ptr;\ int save_version = ptr_p->version;\ if(ptr_p == at->ptr && save_version == ptr_p->version)\ {\ memcpy(ret.base.data,ptr_p->data,at->data_size);\ __asm__ volatile("" : : : "memory");\ if(ptr_p == at->ptr && save_version == ptr_p->version)\ break;\ ATOMIC_INCREASE(&miss_count);\ }\ else\ ATOMIC_INCREASE(&miss_count);\ }\ ATOMIC_INCREASE(&get_count);\ return ret;\ } #define SET_ATOMIC_ST(NAME,TYPE)\ void NAME(struct atomic_type *at,TYPE p)\ {\ struct atomic_st *new_p = at->array[at->index];\ at->index = (at->index + 1)%2;\ memcpy(new_p->data,p.base.data,at->data_size);\ __asm__ volatile("" : : : "memory");\ new_p->version = ++at->g_version;\ __asm__ volatile("" : : : "memory");\ at->ptr = new_p;\ ATOMIC_INCREASE(&set_count);\ }
#include "util/thread.h" #include "util/SysTime.h" #include "util/atomic.h" #include <assert.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include "util/sync.h" #include "util/atomic_st.h" struct point { struct atomic_st base; volatile int x; volatile int y; volatile int z; }; GET_ATOMIC_ST(GetPoint,struct point); SET_ATOMIC_ST(SetPoint,struct point); struct atomic_type *g_points[1000]; void *SetRotine(void *arg) { int idx = 0; int pos = 0; while(1) { struct point p; ++pos; p.x = p.y = p.z = pos+1; SetPoint(g_points[idx],p); idx = (idx + 1)%100; } } void *GetRoutine(void *arg) { int idx = 0; while(1) { struct point ret = GetPoint(g_points[idx]); if(ret.x != ret.y || ret.x != ret.z || ret.y != ret.z) { printf("%d,%d,%d\n",ret.x,ret.y,ret.z); assert(0); } idx = (idx + 1)%100; } } int main() { struct point p; p.x = p.y = p.z = 1; int i = 0; for(; i < 1000; ++i) { g_points[i] = create_atomic_type(sizeof(p)); SetPoint(g_points[i],p); } thread_t t1 = CREATE_THREAD_RUN(1,SetRotine,NULL); thread_t t2 = CREATE_THREAD_RUN(1,GetRoutine,(void*)1); thread_t t3 = CREATE_THREAD_RUN(1,GetRoutine,(void*)1); thread_t t4 = CREATE_THREAD_RUN(1,GetRoutine,(void*)1); uint32_t tick = GetSystemMs(); while(1) { uint32_t new_tick = GetSystemMs(); if(new_tick - tick >= 1000) { printf("get:%d,set:%d,miss:%d\n",get_count,set_count,miss_count); get_count = set_count = miss_count = 0; tick = new_tick; } sleepms(50); } }
上面是测试代码,开启1个写线程和3个读线程,对一个atomic_type进行争抢测试,下面是测试数据的对比,先是无锁方式的,然后是加锁方式:
get:32231360,set:8129332,miss:4922677
get:30698439,set:7218725,miss:5229885
get:30248904,set:7256191,miss:5270275
get:30127294,set:7302881,miss:5312710
get:30450684,set:7325376,miss:5291387
get:30602374,set:7210568,miss:5226397
get:30542229,set:7231159,miss:5212140
get:6829928,set:1897596,miss:0
get:7005253,set:1897336,miss:0
get:7037773,set:1893310,miss:0
get:7121759,set:1907072,miss:0
get:7136176,set:1896144,miss:0
get:7118790,set:1914569,miss:0
get:7096869,set:1913391,miss:0
可以看到如果读写线程都在争抢同一个atomic_type,无锁方式比加锁方式快了4倍左右.
下面是对100个atomic_type争抢:
get:55162216,set:11529743,miss:199049
get:56026601,set:11259984,miss:205584
get:57597092,set:11386090,miss:213338
get:57144449,set:11237366,miss:208721
get:56939791,set:11119571,miss:209850
get:56983208,set:11180208,miss:209922
get:56720621,set:11169635,miss:208338
get:17567303,set:5803621,miss:0
get:17742257,set:5636563,miss:0
get:17702530,set:5621941,miss:0
get:17179876,set:5492159,miss:0
get:16825500,set:5371242,miss:0
get:17936650,set:5715384,miss:0
get:17912971,set:5743810,miss:0
get:17050807,set:5420599,miss:0
加锁方式读的效率只有无锁方式的1/3左右,写效率大概是无锁方式的1/2.