设为首页 加入收藏

TOP

go标准库I/O模型:epoll+多协程
2019-05-24 18:07:57 】 浏览:162
Tags:标准 I/O 模型 :epoll

本文为linux环境下的总结,其他操作系统本质差别不大。本地文件I/O和网络I/O逻辑类似。

epoll+多线程的模型

epoll+多线程模型和epoll 单进程区别、优点

????对比于redis这样典型的epoll+单进程为主的模型,个人理解epoll+多线程模型相对来说,epoll+多线程更利于程序员编写,维护代码,毕竟多线程的模型更符合多数人的逻辑方式。

????例如,单进程下,如果是简单的一问一答方式的服务类型还是OK得,如果服务器对每一个请求都还有至少1次额外的网络I/O操作, 此时如果额外的I/O操作采用同步的方式,无疑将会将主进程阻塞得不偿失,如果也通过事件驱动的方式进行异步I/O,代码的编写和维护成本无疑大大增加。事实上,额外的耗时I/O操作,redis基本都是通过fork一个子进程处理的。

????而多线程的情况下,就可以保证在一部分的请求在I/O阻塞的情况下,服务器还能处理另一部分请求,并且代码写起来更容易,也能好维护。此外多线程也能利用上处理器的多核,但是这方面我没接触过具体的case,体会也不是很明显。

一种简单epoll+多线程模型

????主线程主要负责listen端口并进行access,将acess到的fd放入一个队列里。创建固定个数的处理线程,消费队列里的fd,从fd中读取请求,进行相应处理。处理后的返回结果直接由处理线程返回到客户端 。

????如果处理线程只涉及计算,没有其他阻塞操作的话,不考虑超线程技术的话,创建和CPU核数一样的线程数即可。但是处理线程涉及阻塞操作的话,线程数就要适当增加,以保证时刻都在处理请求而不是全部阻塞。至于要创建多少就要根据实际情况见仁见智了,建少了机器空闲,建多了额外消耗过多的机器资源会反拖累机器处理请求速度。

golang标准网络库通信模型——epoll+协程

????golang的精髓在于协程,个人理解协程的精髓就在于和epoll的配合使得golang能以较低的开发成本,开发出能支持I/O密集场景下的高并发的服务器。(同样场景下,用c/c++理论上肯定能开发出性能更好的服务器,但是目前来看开发成本肯定要高不少)。而计算密集的场景情况下,cpu除了计算还得维护一个go的调度器,显然这并不是go擅长的领域。

????golang自带的网络库的网络通信模型和上述的简单的epoll+多线程模型类似,但是由于golang使用的是协程,拥有自己的调度器,以及能保证创建的内核线程数量不会太多(调度器保持活跃内核线程数和cpu核数一致),使得在内核线程切换带来的消耗大大降低,而用户线程的切换代价和消耗的资源小的多。

golang标准网络库简单探究

对文件描述符fd的封装 —— netFD

????数据结构上只是对socket系统调用返回的fd做一些简单的封装 文件描述符,对应的通信协议等,此外还有一些方法的封装,如将fd加入到epoll里等(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ..)的封装)

????值得一提,如果不自己在go程序中自己进行epoll_create系统调用的话,一个go程序中只会生成一个epoll实例(通过sync.Once的Do方法实现的)

????所以网路库代码粗略层次大概是这样的(以传输层net包为例)

????net包提供给用户直接使用的方法:提供listen、accept、dail(作为客户端连接)等方法封装,操作的的数据结构是newFD

????newFD(fd_unix.go):对文件描述符的封装,同时也负责提供将fd和多路复用实例关联的方法 (获取newFD 一般是调用go自己封装的一个socket函数)

????netpoll.go:对多路复用实例的封装, 相当于代理层,兼容各种平台

????netpoll_*.go: 不同平台的多路复用的系统调用

lisent

????简单来看,listen()时就是调用go自己封装的socket函数,绑定端口并且监听,在go的socket函数里如果没有创建epoll实例的话创建epoll实例, 把listen系统调用返回的fd加入到epoll中。

Dial

????dial返回的interface实际上是由以下struct代理生成的(这个代理能生成TCP UDP IP UNIX文件对应的conn)

type Dialer struct {
    Timeout time.Duration  // 网络超时时间
    Deadline time.Time     // 连接失效时间 这是一个时间点
    LocalAddr Addr               // 本地地址
    DualStack bool               // 似乎是 该链接是否同时支持ipv4 ipv6 对这个字段没有深刻体会
    FallbackDelay time.Duration  // ipv6连接失败 回退为ipv4协议的时间
    KeepAlive time.Duration  // 连接保持时间,如果不设置以操作系统/通信协议规定的为准
    Resolver *Resolver       // 解析地址的备用解析器
    Cancel <-chan struct{}   // 打断连接生成的一个chan 不建议使用这个字段
    Control func(network, address string, c syscall.RawConn) error  // 创建连接但是没有发出报文前调用的控制函数 有点儿钩子函数的意思
}

????具体生成过程其实步骤逻辑不算少 但是最后也是调用go自己封装的socket函数, 把生成的连接add到epoll实例里面。

read/accept 读操作阻塞时

?????会直接读对应newFd里存储的系统fd, 因为是noblock,所以read()会直接返回, 如果read系统调用返回的是syscall.EAGAIN, 则会调用gopark()将对应的goruntine挂起(设置_Gwaiting状态)。

write阻塞时

????如果第一次write系统调用就将要写入内容全部写入fd,就不会阻塞,否则同样将goruntine挂起。

epoll通知被阻塞的goruntine启动

?????go的runtime线程在启动时如果,有使用网路库的话,会调用netpoll函数,在linux环境下实际上调用的就是epoll_wait(阻塞调用), 响应事件的回调函数就是调用goready唤醒该fd对应的goruntine(置为runnnable)。每一次epoll_wait最多返回128个事件。

总结

????所以go的标准库网络通信模型其实还是比较简单简洁的,而其核心在其天然支持协程的特性,利用自己的调度器将传统多线程在线程数多的情况造成的系统消耗降低到一个比较令人满意的程度,从而达到一个开放效率和运行效率上的平衡。

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇xorm-创建时间created 下一篇函数

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目