socket服务器多线程优化

在单线程环境下一个线程虽然可以执行任务但是所有的任务都交给一个线程来做当任务积累起来时,前面的任务会影响后续任务的执行,并且现在都是多核处理器我们需要尽可能利用cpu所以多线程 的优化就是一个不错的选择。

我们选择多线程后可以对任务进行分类,一个线程只执行某一特定任务,比如我们设置boss线程来处理客户端的连接请求,设置worker线程来处理客户端的读写操作等

但是引入多线程后线程间的执行顺利我们需要控制,比如当boss线程接收到一个请求后执行**SocketChannel sc = ssc.accept();**语句获取到连接,但是还需要将这个连接交给worker线程来处理读写请求,这就要求我们一定存在worker线程并且正在工作,但是工作状态的worker线程在没有任务进来的情况下会被多路选择器(selector)阻塞在select方法处,而我们又需要将连接sc注册到worker的selector上让它去监听这个连接上的事件,由于selector被阻塞我们无法完成注册,这就尬住了。

解决方法就是我们控制register和select方法,使register方法能再select方法执行后执行,我们这时可以将register方法放到worker线程的注册方法中执行,并且唤醒一次selector就可以执行到register了

java 复制代码
package cn.itcast.mytest;

import javafx.concurrent.Worker;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;

import static cn.itcast.nio.c2.ByteBufferUtil.debugAll;

@Slf4j
public class MultiThreadServer {
    public static void main(String[] args) throws IOException {
        //创建一个boss线程,专门用来监听有没有连接请求
        Thread.currentThread().setName("boss");
        //开启服务器
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));
        //创建一个selector
        Selector boss = Selector.open();
        SelectionKey bossKey = ssc.register(boss, SelectionKey.OP_ACCEPT, null);

        //创建多个worker线程(固定数量的),专门用来处理读写事件
        Worker[] workers = new Worker[2];
        for (int i = 0; i < workers.length; i++) {
            workers[i] = new Worker("worker-" + i);
        }

        //不断轮询看是否有新客户接入
        while(true){
            boss.select();
            //有新连接接入
            Iterator<SelectionKey> iterator = boss.selectedKeys().iterator();
            while(iterator.hasNext()){
                SelectionKey key = iterator.next();
                //删除任务
                iterator.remove();
                if(key.isAcceptable()){
                    //获取连接
                    SocketChannel sc = ssc.accept();
                    sc.configureBlocking(false);
                    //将连接交给worker线程处理
                    int index = (int) (sc.hashCode() % workers.length);
                    log.debug("分配给worker-{}", index);
                    workers[index].register(sc);//这个方法是在boss线程中被调用的,所以register方法中的内容实际还是由boss线程执行
                    log.debug("connected...{}", sc.getRemoteAddress());
                }
            }
        }
    }

    static class Worker implements Runnable{
        private Thread thread;
        private Selector worker;
        private String name;
        private boolean start = false;//保证我们一个worker线程只创建一次线程,避免创建太多线程
        private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();

        public Worker(String name) {
            this.name = name;
        }

        public void register(SocketChannel sc) throws IOException {
            if (!start) {
                this.thread = new Thread(this, name);
                this.worker = Selector.open();
                thread.start();
                start = true;
            }
            //将channel注册到worker的selector上,为了这条语句不被select()阻塞我们需要他在select()方法前执行,所以最好是在同一个线程被执行这样我们容易控制先后关系
            //通过线程安全的队列来实现,向队列中添加任务但先不执行
            queue.add(() -> {
                try {
                    sc.register(worker, SelectionKey.OP_READ, null);
                } catch (ClosedChannelException e) {
                    throw new RuntimeException(e);
                }
            });
            //因为在thread.start();后已经执行了worker.select();方法所以我们需要主动唤醒select来执行register
            worker.wakeup();
        }

        @Override
        public void run() {
            //worker的职责就是专门关注读写事件
            while(true) {
                try {
                    //监听
                    worker.select();
                    Runnable task = queue.poll();
                    if(task != null){
                        task.run();//执行注册任务
                    }
                    //获取事件集
                    Iterator<SelectionKey> iterator = worker.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey workerKey = iterator.next();
                        iterator.remove();
                        if (workerKey.isReadable()) {
                            //读取数据
                            try {
                                ByteBuffer buffer = ByteBuffer.allocate(16);
                                SocketChannel sc = (SocketChannel) workerKey.channel();
                                log.debug("read...{}", sc.getRemoteAddress());
                                int read = sc.read(buffer);
                                if (read == -1) {//客户端断开连接
                                    workerKey.cancel();
                                } else {
                                    buffer.flip();
                                    debugAll(buffer);
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }
    }
}
相关推荐
子榆.6 分钟前
CANN ATC编译器:模型从Python到达芬奇指令走了多远
开发语言·python·neo4j
lookaroundd7 分钟前
llm-compressor 普通量化调用链分析
python·算法
Loo国昌10 分钟前
从 Agent 编排到 Skill Runtime:企业 AI 工程化的下一层抽象
大数据·人工智能·后端·python·自然语言处理
小羊在睡觉21 分钟前
力扣239. 滑动窗口最大值
数据结构·后端·算法·leetcode·go
编码者卢布22 分钟前
【Azure Service Bus】Azure Service Bus Java SDK 中 Token 刷新异常的排查思路
java·python·azure
liuyunshengsir26 分钟前
PyTorch 最小模型转 ONNX 完整样例
人工智能·pytorch·python
z2023050828 分钟前
RDMA之RoCEv2 无损网络PFC 、DCQCN 和ECN (7)
linux·服务器·网络·人工智能·ai
不吃土豆的马铃薯31 分钟前
Spdlog 入门:日志记录器与日志槽基础详解
服务器·开发语言·c++·c·日志·spdlog
我星期八休息40 分钟前
Linux系统编程—库制作与原理
linux·运维·服务器·数据结构·人工智能·python·散列表
William.csj43 分钟前
服务器——交互式 NVIDIA GPU 监控工具
运维·服务器