前言:
目前在写一个简易版的新人游戏服务器demo,使用的是netty框架,由于netty是Reactor模型,由boss线程池进行对于套接字是否就绪的监听,work线程池对于io流进行操作。前面一直将游戏逻辑放在了work线程池中处理,这样子如果遇到一些耗时大的逻辑会影响netty框架对于io流的读取。这里就将新建一个逻辑线程池进行逻辑处理,并且串行化执行对于同一个角色的数据修改。
问题:
为什么要串行化对于角色数据修改?
对于游戏来说,角色的血量、蓝量。这些可能会被其他角色的攻击、buffer、技能等等所修改,所以这些共享的变量通常都需要注意并发修改。最普遍的做法就是加上锁。
对此有另一种更加好的做法。让角色血量、蓝量的数据修改串行化执行。这样子就不会存在并发问题,并且只需加上voliate关键字。即可让其他线程也可以获取到最新的值。
为什么将业务逻辑执行与work线程池分离?
work线程池是netty框架对于io流进行读写的线程组。将游戏逻辑放在了work线程池中处理,这样子如果遇到一些耗时大的逻辑会影响netty框架对于io流的读取
如何实现?
前提:
ChannelHandlerContext.write的源码:
private void write(Object msg, boolean flush, ChannelPromise promise) {
AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
if (flush) {
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
} else {
AbstractWriteTask task;
if (flush) {
task = WriteAndFlushTask.newInstance(next, m, promise);
} else {
task = WriteTask.newInstance(next, m, promise);
}
safeExecute(executor, task, promise, m);
}
}
Netty允许在非NIO线程中写消息的。如果当前是在NIO线程,就直接写过去,如果不在NIO线程,写消息操作会被封装成一个task,然后再由NIO线程池来处理。
这样子就给了我们操作空间了,我们只需要在自身的逻辑线程池中wirte()即可让netty自动的转移到work线程组中执行。
功能实现:
结构图
主要是通过channel的hashcode后对于线程组数量求余获取其下标,每次通过下标提交到不同线程中的任务队列里即可。一个channel对应一个角色,所以每次该角色的数据修改都在同一个线程中,根据任务队列先进先出实现串行化执行。
2021-2-7 修改:为了保证用户在同一个客户端登陆多个用户,以及切换角色,最终修改为采用playId为求余下标。
代码实现:
自定义线程池接口:
/**
* 自定义线程池接口
* @author lqhao
*/
public interface ThreadPool<Job extends Runnable> {
/**
* 执行一个Job,这个Job需要实现Runnabl
*/
void execute(Job job,Integer index);
/**
* 关闭线程池
*/
void shutdown();
}
逻辑线程池的实现&内部线程类
/**
* 逻辑线程池的实现
* @author lqhao
*/
public class LogicThreadPool<Job extends Runnable>implements ThreadPool<Job> {
private static LogicThreadPool instance;
private int threadSize;
public static void init(int num){
LogicThreadPool logicThreadPool=new LogicThreadPool();
instance=logicThreadPool;
instance.initializeWorkers(num);
instance.threadSize=num;
}
public int getThreadSize() {
return threadSize;
}
public void setThreadSize(int threadSize) {
this.threadSize = threadSize;
}
public static LogicThreadPool getInstance() {
return instance;
}
/**
* 工作者列表
*/
private final CopyOnWriteArrayList<Worker> workers=new CopyOnWriteArrayList<>();
/**
*线程编号生成
*/
private AtomicLong threadNum = new AtomicLong();
/**
* 初始化线程工作者
*/
private void initializeWorkers(int num) {
for (int i = 0; i < num; i++) {
Worker worker = new Worker();
workers.add(worker);
Thread thread = new Thread(worker, "ThreadPool-Worker-" + threadNum.incrementAndGet());
thread.start();
}
}
class Worker implements Runnable {
private final Logger log = LoggerFactory.getLogger(ServerHandler.class);
private volatile boolean running = true;
private LinkedList<Job> tasks = new LinkedList<>();
@Override
public void run() {
Job job = null;
while (running) {
//如果线程中的工作队列空了,就wait
synchronized (tasks) {
while (tasks.isEmpty()) {
try {
tasks.wait();
} catch (InterruptedException ex) {
// 感知到外部对WorkerThread的中断操作,返回
Thread.currentThread().interrupt();
return;
}
}
// 取出一个Job
job = tasks.removeFirst();
}
if (job != null) {
try {
job.run();
} catch (Exception ex) {
log.error("System.out...... -> " + ex);
}
}
}
}
public void shutdown() {
running = false;
}
}
@Override
public void execute(Job job,Integer index) {
if (job != null) {
// 添加一个工作,然后进行通知
//根据channel的HashCode取余
Worker worker=workers.get(index);
synchronized (worker.tasks) {
worker.tasks.addLast(job);
//通知线程有任务了
worker.tasks.notifyAll();
}
}
}
@Override
public void shutdown() {
for (Worker worker : workers) {
worker.shutdown();
}
}
}
这里需要处理的是对于任务队列的出队入队都需要并发安全处理,即便它和线程是一对一的关系,但是还是存其他线程往里面放任务的情况,还是会有并发情况。
调用代码:
//根据channel计算index
Integer index= CommonsUtil.getIndexByChannel(ctx.channel());
//放入逻辑线程池中执行 参数Job、index
LogicThreadPool.getInstance().execute(new Runnable() {
@Override
public void run() {
try {
dispatcherservlet.handler(request,ctx.channel());
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
},index);
总结:
这部分如果一开始来写可能会忽略掉任务队列的并发安全,不过之前看《java并发艺术》这本书的时候有写过一个相关的demo,这里的部分代码都是参考了那本书。串行化执行,这是一种新的解决并发安全的方法吧。减少了锁的使用,但是可能会造成的影响就是逻辑线程池里部分线程繁忙,而部分线程一直空闲。