Go语言为什么适合开发网络服务?

2023-09-20 16:46:24 浏览数 (1)

在互联网迅猛发展的数十年里,我们不断面临新的场景与挑战,例如大数据、大规模集群计算、复杂的网络环境、多核处理器对于高并发的需求、云计算、上千万行的服务器代码等,那些成熟但“上了年纪”的语言不能为新的场景给出直接的解决方案,此时,Go语言应运而生了。

Go语言因其简洁、高效,以及良好的并发处理能力成为编程语言中一颗冉冉升起的新星,被广泛应用于网络服务开发,这其中关键在于Go语言对于协程调度、同步编程模式、非阻塞I/O,以及I/O多路复用的独特处理方式。让我们一起从实践和源码角度探讨Go语言如何实现高并发网络服务。

协程调度

在多核时代,Go语言在线程之上引入了轻量级的协程。作为并发原语,协程解决了传统多线程开发中的诸多问题,例如内存屏障、死锁等,并降低了线程的时间成本与空间成本。

线程的时间成本主要来自切换线程上下文时,开发者态与内核态的切换、线程的调度、寄存器变量以及状态信息的存储。

线程的空间成本主要来自线程的堆栈大小。线程的堆栈大小一般是在创建时指定的,为了避免出现栈溢出(Stack Overflow),默认的栈会相对较大(例如2MB),这意味着每创建 1000 个线程就需要消耗2GB 的虚拟内存,将大大限制创建线程的数量。而 Go 语言中的协程栈大小默认为2KB,并且是动态扩容的,因此在实践中,经常会看到成千上万个协程存在。并且不需要担忧性能问题。

代码语言:javascript复制
// 源码中初始的栈大小
_StackMin = 2048

同步编程模式

Go语言借助协程提供了一种非常直观的同步编程模式。如下为一个典型的网络服务器,main函数中监听新的连接,每一个新建立的连接都会新建一个协程执行Handle函数。而这个处理函数,完全是阻塞模式的,无需使用复杂的回调或信号处理机制,让代码的阅读和编写都变得更直观、简单。

代码语言:javascript复制
func main() {
   listen, err := net.Listen("tcp", ":8888")
   for {
      conn, err := listen.Accept()
      // 开启新的Groutine,处理新的连接
      go Handle(conn)
   }
}


func Handle(conn net.Conn) {
   defer conn.Close()
   packet := make([]byte, 1024)
   for {
      // 阻塞直到读取数据
      n, err := conn.Read(packet)
      // 阻塞直到写入数据
      _, _ = conn.Write(packet[:n])
   }
}

非阻塞I/O

Go表面上实现了同步编程模式,但背后在处理Socket时使用的是非堵塞模式。

这意味着,协程在遇到阻塞时,实际上并不会阻塞线程,而只是陷入用户态的等待。如下图GMP模型描述了协程(G)、线程(M)与逻辑处理器P之间的关系。

Go运行时有强大的调度器,当某个协程G阻塞时,其他可运行的协程借助逻辑处理器P仍然可以被调度到线程M上执行。这种设计在保持编程简单性的同时,确保了高并发性能。

I/O多路复用

Go语言对I/O多路复用的封装,是它高效处理大规模Socket的关键。当协程阻塞等待Socket数据时,Go语言可以使用I/O多路复用技术监听大量Socket的变化。

在Go中,这种多路复用机制被称作netpoll。

netpoll封装了针对不同操作系统的多路复用API(如epoll/kqueue/iocp)。比如在Linux系统中,netpoll实现的是epoll,它在处理大规模Socket时的性能显著优于select和poll。Go语言对netpoll机制进行了封装,主要包括以下函数:

代码语言:javascript复制
// netpoll_epoll.go
func netpollinit()
func netpollopen(fd uintptr, pd *pollDesc) int32
func netpoll(delay int64) gList

Go运行时只会全局调用一次netpollinit函数。而我们之前看到的conn.Read、conn.Write等读取和写入函数底层都会调用netpollopen函数将对应的Socket放入epoll中进行监听。

程序可以定期调用netpoll函数以获取已就绪的Socket。netpoll函数的主要调用时机有两个:一是在系统监控过程中的定期检查,二是在调度函数执行过程中。要注意的是,netpoll函数处理Socket时使用的是非堵塞模式,这也意味着Go网络模型不会让阻塞陷入操作系统调用中。而强大的调度器又保证了用户协程陷入堵塞时可以轻松地切换到其他协程运行,保证了用户协程公平且充分地执行。这就让Go语言在处理高并发的网络请求时仍然具有简单与高效的特性。

最后,我们可以把Go语言的致胜法宝可以总结为一个公式:同步编程 多路复用 非阻塞I/O 协程调度。

0 人点赞