当前位置: 首页 > news >正文

结合Golang语言说明对多线程编程以及 select/epoll等网络模型的使用

首先介绍select和epoll这两个I/O多路复用的网络模型,然后介绍多线程编程,最后结合Go语言项目举例说明如何应用

一、select 和 epoll 的介绍

1. select 模型

select 是一种I/O多路复用技术,它允许程序同时监视多个文件描述符(通常是套接字),等待一个或多个描述符就绪(可读、可写或异常)然后进行相应的操作,它的跨平台兼容性好(Windows/Linux/macOS)

核心原理​​:

  • 使用 fd_set 数据结构管理文件描述符集合
  • 通过 select() 系统调用阻塞监听多个文件描述符
  • 每次调用都需要重新传递所有描述符集合

select的缺点:

  • 支持的文件描述符数量有限(通常1024,FD_SETSIZE限制
  • 每次调用都需要将文件描述符集合从用户态拷贝到内核态开销大
  • 内核遍历所有文件描述符(O(n)复杂度)来检查就绪状态,效率不高
  • 需要手动维护描述符集合
//c语言fd_set read_fds;
int max_fd = 0;// 添加 socket 到监控集
FD_ZERO(&read_fds);
FD_SET(sockfd, &read_fds);
max_fd = (sockfd > max_fd) ? sockfd : max_fd;while(1) {fd_set tmp_fds = read_fds;int ret = select(max_fd + 1, &tmp_fds, NULL, NULL, NULL);for (int i = 0; i <= max_fd; i++) {if (FD_ISSET(i, &tmp_fds)) {if (i == sockfd) {// 处理新连接} else {// 处理客户端数据}}}
}

2. epoll 模型

epoll 是Linux下高性能的I/O多路复用机制,解决了select的缺点

​核心原理​​:

  • 使用 epoll_create 创建 epoll 实例
  • 通过 epoll_ctl 动态添加/删除文件描述符
  • 通过 epoll_wait 获取就绪事件

特点: 

  • 支持的文件描述符数量不受限制,海量并发连接(仅受系统最大打开文件数限制(系统内存限制))
  • 使用事件驱动避免遍历所有文件描述符(O(1)事件复杂度)
  • 通过内存映射技术避免用户态和内核态之间频繁拷贝
  • Linux 专有高性能模型
  • 支持边缘触发(ET)和水平触发(LT)模式

epoll有两种工作模式:

  • LT(Level Trigger):水平触发,只要文件描述符就绪,就会触发通知(默认)
  • ET(Edge Trigger):边缘触发,只有状态变化时触发通知效率更高,但要求用户必须一次性处理完所有数据
//c语言int epfd = epoll_create1(0);
struct epoll_event ev, events[MAX_EVENTS];// 添加 socket
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);while(1) {int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);for (int i = 0; i < nfds; i++) {if (events[i].data.fd == sockfd) {// 接收新连接int client = accept(sockfd, ...);ev.data.fd = client;epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev);} else {// 处理客户端数据recv(events[i].data.fd, ...);}}
}

二、多线程编程

多线程允许程序同时运行多个任务,线程共享进程的地址空间资源每个线程有自己的栈和寄存器。多线程编程需要注意:

  • 线程同步使用互斥锁(mutex)、条件变量(condition variable)、信号量防止竞态条件
  • 死锁:避免多个线程互相等待对方释放锁
  • 线程安全:确保多个线程同时执行同一段代码时不会产生问题

典型应用场景​

  • CPU 密集型任务(如视频编码)
  • 阻塞操作处理(如文件 I/O)
  • 多核并行计算
// ​​C++ 线程池示例​​class ThreadPool {
public:ThreadPool(size_t threads) {for(size_t i=0; i<threads; ++i)workers.emplace_back([this]{while(true) {std::function<void()> task;{std::unique_lock<std::mutex> lock(queue_mutex);condition.wait(lock, [this]{ return stop || !tasks.empty(); });if(stop && tasks.empty()) return;task = std::move(tasks.front());tasks.pop();}task();}});}template<class F>void enqueue(F&& f) {{std::unique_lock<std::mutex> lock(queue_mutex);tasks.emplace(std::forward<F>(f));}condition.notify_one();}~ThreadPool() {{std::unique_lock<std::mutex> lock(queue_mutex);stop = true;}condition.notify_all();for(std::thread &worker: workers)worker.join();}private:std::vector<std::thread> workers;std::queue<std::function<void()>> tasks;std::mutex queue_mutex;std::condition_variable condition;bool stop = false;
};// 使用示例
ThreadPool pool(4);
pool.enqueue([]{ processTask(); });

三、Go语言中的应用

Go语言通过goroutinechannel提供并发支持。goroutine是轻量级线程,由Go运行时管理,开销小。同时,Go提供了select语句用于处理多个channel的通信,Go语言的网络模型基于非阻塞I/O和I/O多路复用(底层使用epoll/kqueue等),通过goroutine和channel实现高并发

Go 语言并发模型特点​:

  • ​Goroutine​​:轻量级线程(协程),初始栈仅 2KB
  • ​Channel​​:类型安全的通信管道,解决数据竞争问题
  • ​select 语句​​:监听多个 channel 操作
  • ​Epoll 集成​​:net 包底层自动使用 epoll/kqueue
  • ​GMP 调度器​​:高效管理百万级 goroutine

示例1:使用select处理多个channel 

package mainimport ("fmt""time"
)func main() {ch1 := make(chan string)ch2 := make(chan string)go func() {time.Sleep(1 * time.Second)ch1 <- "one"}()go func() {time.Sleep(2 * time.Second)ch2 <- "two"}()for i := 0; i < 2; i++ {select {case msg1 := <-ch1:fmt.Println("received", msg1)case msg2 := <-ch2:fmt.Println("received", msg2)}}
}

 示例2:使用goroutine和net包实现高并发TCP服务器

package mainimport ("bufio""fmt""net""strings"
)func handleConnection(conn net.Conn) {defer conn.Close()fmt.Println("Accepted connection from", conn.RemoteAddr())reader := bufio.NewReader(conn)for {message, err := reader.ReadString('\n')if err != nil {fmt.Println("Connection closed by client")return}fmt.Print("Message received:", string(message))response := strings.ToUpper(message)conn.Write([]byte(response))}
}func main() {listener, err := net.Listen("tcp", ":8080")if err != nil {fmt.Println("Error listening:", err)return}defer listener.Close()fmt.Println("Server listening on :8080")for {conn, err := listener.Accept()if err != nil {fmt.Println("Error accepting connection:", err)continue}go handleConnection(conn) // 每个连接一个goroutine}
}

示例3:使用epoll(Go的net包已经封装,但可以通过syscall包直接使用epoll,仅作演示)

在Go中,通常不直接使用epoll,而是利用net包和goroutine的并发模型,但下面是一个直接使用epoll的简单示例:

package mainimport ("fmt""golang.org/x/sys/unix""net""os"
)const maxEvents = 10func main() {// 创建socketfd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, 0)if err != nil {fmt.Println("Error creating socket:", err)os.Exit(1)}defer unix.Close(fd)// 设置地址重用err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)if err != nil {fmt.Println("Error setting SO_REUSEADDR:", err)os.Exit(1)}// 绑定地址addr := unix.SockaddrInet4{Port: 8080,Addr: [4]byte{0, 0, 0, 0},}err = unix.Bind(fd, &addr)if err != nil {fmt.Println("Error binding:", err)os.Exit(1)}// 监听err = unix.Listen(fd, maxEvents)if err != nil {fmt.Println("Error listening:", err)os.Exit(1)}// 创建epoll实例epollFd, err := unix.EpollCreate1(0)if err != nil {fmt.Println("Error creating epoll instance:", err)os.Exit(1)}defer unix.Close(epollFd)event := unix.EpollEvent{Events: unix.EPOLLIN,Fd:     int32(fd),}err = unix.EpollCtl(epollFd, unix.EPOLL_CTL_ADD, fd, &event)if err != nil {fmt.Println("Error adding fd to epoll:", err)os.Exit(1)}var events [maxEvents]unix.EpollEventfor {n, err := unix.EpollWait(epollFd, events[:], -1)if err != nil {fmt.Println("Error in EpollWait:", err)continue}for i := 0; i < n; i++ {if int(events[i].Fd) == fd {// 有新的连接connFd, _, err := unix.Accept(fd)if err != nil {fmt.Println("Error accepting connection:", err)continue}connEvent := unix.EpollEvent{Events: unix.EPOLLIN | unix.EPOLLET, // ET模式Fd:     int32(connFd),}err = unix.EpollCtl(epollFd, unix.EPOLL_CTL_ADD, connFd, &connEvent)if err != nil {fmt.Println("Error adding connFd to epoll:", err)unix.Close(connFd)continue}fmt.Println("Accepted new connection")} else {// 连接有数据可读connFd := int(events[i].Fd)buf := make([]byte, 1024)n, err := unix.Read(connFd, buf)if err != nil || n == 0 {// 连接断开unix.Close(connFd)fmt.Println("Connection closed")continue}fmt.Printf("Read %d bytes from connFd %d: %s\n", n, connFd, string(buf[:n]))// 回写unix.Write(connFd, buf[:n])}}}
}

示例4: WebSocket 服务器​

package mainimport ("net/http""github.com/gorilla/websocket""sync"
)var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
var connections sync.Mapfunc wsHandler(w http.ResponseWriter, r *http.Request) {conn, _ := upgrader.Upgrade(w, r, nil)defer conn.Close()// 注册连接connections.Store(conn, true)defer connections.Delete(conn)for {// 消息处理(非阻塞读取)_, msg, err := conn.ReadMessage()if err != nil {break}// 广播消息(并发安全)connections.Range(func(k, v interface{}) bool {if client, ok := k.(*websocket.Conn); ok {go func() { // 每个发送使用独立goroutineclient.WriteMessage(websocket.TextMessage, msg)}()}return true})}
}func main() {// 配置epoll网络模型(自动生效)http.HandleFunc("/ws", wsHandler)// 启动4个工作线程处理网络I/Ofor i := 0; i < 4; i++ {go func() {http.ListenAndServe(":8080", nil)}()}select {} // 永久阻塞
}

注意:在Go中,通常使用net包而不是直接调用系统调用,因为net包已经高效地封装了I/O多路复用,并且配合goroutine更易于管理

四.核心优化技术说明​

1.​​Epoll 自动化​

  • Go 的 net 包自动使用最佳系统调用
  • Linux 使用 epoll,macOS 使用 kqueue
  • 通过 netpoll 实现高效 I/O 多路复用

​2.Goroutine 管理​

  • 每个连接独立 goroutine 处理
  • 使用 sync.Map 实现并发安全的连接池
  • Range 方法避免全局锁竞争

​3.Channel 工作池​​ 

func worker(jobs <-chan Message) {for msg := range jobs {process(msg) // 业务处理}
}func main() {// 创建带缓冲的ChanneljobQueue := make(chan Message, 1000) // 启动工作池for i := 0; i < runtime.NumCPU(); i++ {go worker(jobQueue)}// 接收消息时投递go func() {for msg := range messageChannel {select {case jobQueue <- msg: // 正常投递default: // 队列满时丢弃消息log.Println("Job queue full!")}}}()
}

4.​​性能优化要点​

// 1. 调整调度器参数
runtime.GOMAXPROCS(12) // 设置使用的CPU核心数// 2. 对象复用池减少GC压力
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, 1024) },
}// 3. 流量控制
rateLimiter := make(chan struct{}, 1000) // 限制并发处理数// 4. 开启TCP快速打开
ln, _ := net.ListenConfig{FastOpen: true,KeepAlive: 30 * time.Second,
}.Listen("tcp", ":80")

五.总结

性能对比表

模型并发处理能力CPU占用内存开销编程复杂度
多线程+select1K~10K中等
epoll+线程池100K+中等
Go+Goroutine1M+极低
  • select和epoll都是I/O多路复用机制,epoll效率更高
  • 多线程编程需要注意同步、死锁和线程安全
  • Go语言通过goroutine和channel实现高并发,网络模型底层使用epoll等,但通过net包封装,简化了编程

在Go项目中,通常使用goroutine处理并发连接,每个连接一个goroutine(示例2),或者使用worker池来避免大量goroutine的创建(如果连接数非常多)。同时,可以利用select语句处理多个channel的通信(示例1)。直接使用epoll的情况较少(示例3主要用于理解底层机制),因为标准库已经提供了很好的抽象

最佳实践建议​

  • Linux 环境直接采用 Go 的 net 包实现高并发
  • CPU 密集型任务配合 runtime.GOMAXPROCS() 调优
  • 使用 pprof 监控 goroutine 泄漏问题
  • 敏感数据操作采用 sync/atomic 避免锁竞争
  • 利用 io.Reader 和 bytes.Buffer 实现零拷贝处理
http://www.lryc.cn/news/601377.html

相关文章:

  • goland编写go语言导入自定义包出现: package xxx is not in GOROOT (/xxx/xxx) 的解决方案
  • 学习Python中Selenium模块的基本用法(1:简介)
  • Day06–哈希表–242. 有效的字母异位词,349. 两个数组的交集,202. 快乐数,1. 两数之和
  • 仓库管理系统-2-后端之基于继承基类的方式实现增删改查
  • 7.25 C/C++蓝桥杯 |排序算法【下】
  • macOS 安装 Homebrew
  • JavaScript事件(event)对象方法与属性
  • mac配置多版本jdk
  • C#中Visual Studio平台按照OfficeOpenXml步骤
  • Min-Max标准化​ 和 ​Z-score标准化
  • Python队列算法:从基础到高并发系统的核心引擎
  • LeetCode|Day27|70. 爬楼梯|Python刷题笔记
  • Spring Retry 异常重试机制:从入门到生产实践
  • Spring Boot自动配置原理深度解析
  • 适配IE11(通过Babel+core-js转译ES6语法)
  • Flutter 生命周期介绍
  • 几个注册中心的特性
  • 欧拉图与欧拉回路
  • 菜鸟的C#学习(四)
  • windows 10安装oracle(win64_11gR2)
  • 医疗AI语义潜空间分析研究:进展与应用
  • Unity 实时 CPU 使用率监控
  • IP--MGER综合实验报告
  • Linux驱动20 --- FFMPEG视频API
  • 回归预测 | MATLAB实现BiTCN双向时间卷积神经网络多输入单输出回归预测
  • AWS免费套餐全面升级:企业降本增效与技术创新解决方案
  • 《频率之光》
  • 详解赛灵思SRIO IP并提供一种FIFO封装SRIO的收发控制器仿真验证
  • 基于Django的天气数据可视化分析预测系统
  • Django实时通信实战:WebSocket与ASGI全解析(下)