1.3.2 强大的runtime
Go语言具有强大的runtime,支持语言级别的并发、高效的内存管理和自动垃圾回收机制。
1.语言级别的并发
一些编程语言需要通过包装库来实现并发,这可能会降低代码的可读性。然而,Go在语言层面就实现了并发,Go语言的并发特性体现在能够充分利用多核提高CPU的并发利用率上,并且从单核转换为多核的成本也较低。编写并发代码的过程很简单,即使是刚接触Go语言的开发者也能轻松编写出高并发程序,这也是众多开发人员选择Go语言的原因之一。
下面的代码用于开启1000个协程(goroutine),且并发输出传入的i值。
func Print(i int) {
fmt.Println("goroutine:", i)
}
func main() {
for i := 0; i < 1000; i++ {
go Print(i)
}
time.Sleep(time.Second)
}
从上述代码可知,使用Go语言进行并发编程非常简单,仅仅使用一个关键字go即可。其中涉及的CPU绑定、时间切片优化等工作,已由Go语言底层进行了封装。另外,Go并发源码的可读性非常强。
Go语言能实现高并发得益于其实现的GPM调度模型。G、P、M这三个字母分别指模型中的协程、逻辑处理器和工作线程,Go语言在用户层实现了GPM调度模型。Go语言中的协程是轻量级的线程,每个协程占用约2KB的内存,因此可以有效利用多核进行大规模的并发处理。在Go语言中实现并发的原因很简单,就是为了高效地完成协程对CPU资源的调度。因此,Go语言调度的核心是根据特定的算法将协程投放到操作系统的线程中执行。因为Go程序在runtime中就实现了自己的调度器,所以我们常说Go在语言级别支持并发性。
在高并发场景中,可能会出现临界资源安全(数据冲突)问题。为了解决这个问题,除了使用传统的锁,Go语言还实现了极具特色的通信顺序进程(Communicating Sequential Processes,CSP)模型。该模型吸收了管道通信机制的优点,形成了独特的通道(channel)数据类型。
2.高效的内存管理
Go语言的内存管理借鉴了Google公司为C语言开发的TCMalloc算法。TCMalloc算法的核心思想是将内存分级管理,从而降低锁的粒度,还可以通过分配连续的内存来减少内存碎片。它可以替代系统中与内存分配相关的函数(如malloc、free、new等)。Go语言借鉴这个思路,将内存按照大小分配给不同的对象,并通过一些高效的内存分配算法来实现内存管理。内存分配、管理的大致过程如下。
(1)程序启动时,向操作系统申请一大块虚拟内存。
(2)根据规则将申请的虚拟内存切割成小的内存块,并根据对象的大小分配对应规格的内存块。
(3)回收时不直接将空闲内存返还给操作系统,而是自己管理,若有新对象进入内存并提出分配申请,则直接进行分配,从而减轻操作系统的工作负担。
内存使用多级分配的管理方式,每个线程维护自己的本地内存池。进行内存分配时,优先从本地内存池中分配,这种方式不需要加锁,可以避免或减少因频繁向全局内存池申请内存而引发的竞争。当本地内存池中的内存不足时,再向上一级内存池申请。
3.自动垃圾回收机制
C语言虽然性能好,但对开发人员的要求也很高,因为开发人员需要负责管理内存(包括分配和释放内存等),稍不注意就可能导致内存泄漏等问题。在Go语言中,栈和堆是Go程序在运行过程中分配和管理内存的两个主要区域,它们有各自的特点且承担着不同的责任。对于栈中的数据,系统会在当前函数执行结束后自动清理;对于堆中的数据,则通过自动垃圾回收机制统一回收管理。当然,自动进行垃圾回收的语言其性能赶不上手动管理内存的语言。
自动垃圾回收机制的好坏取决于STW的长短。Go语言使用三色标记、混合写屏障、并发增量回收等机制来提高垃圾回收的性能。在Go1.14版本中,垃圾回收的时间已经达到了亚毫秒级。