内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

Go学习笔记(下卷):初始化

2018-06-07 17:36 出处:清屏网 人气: 评论(0

初始化

与命令行参数有关的内容不值得花心思,略过。而与操作系统相关的,仅仅是确定 CPU 处理器数量。

在并发编程时,CPU 处理器数量是重要的决定性参数。它决定了我们应该采取什么样的并行策略,甚至会影响架构设计。也因为如此,我们要知道相关函数(runtime.NumCPU)返回的是物理核数量,还是包含超线程(Hyper-Threading)的结果。

超线程技术是利用特殊指令,在单个物理核内虚拟多个逻辑处理器。这有点像多线程,将等待时间挖掘出来执行其他任务,以提升整体性能。可问题在于,多个逻辑处理器毕竟共享某些资源,某些时候可能适得其反拖累执行效率,比如缓存刷新等等。

程序员应该对硬件体系,以及操作系统有些基本认识。

runtime2.go

var ncpu int32

os_linux.go

func osinit() {
    ncpu = getproccount()       // 返回逻辑处理器数量。
}

debug.go

// returns the number of logical CPUs usable by the current process.
func NumCPU() int {
    return int(ncpu)
}

相比之下,调度器初始化内容就丰富得多。与之有关的内容包括内存管理、垃圾回收,以及并发任务数量。这些内容在后续章节详叙,此处只了解初始化都做了些什么即可。

proc.go

// The bootstrap sequence is:
//
//    call osinit
//    call schedinit
//    make & queue new G
//    call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {

    // M 最大数量限制。
    sched.maxmcount = 10000

    // 内存相关初始化。
    stackinit()
    mallocinit()

    // M 相关初始化。
    mcommoninit(_g_.m)

    // 存储命令行参数和环境变量。
    goargs()
    goenvs()

    // 解析 GODEBUG 的调试参数。
    parsedebugvars()

    // 初始化垃圾回收器。
    gcinit()

    // 初始化 poll 时间。
    sched.lastpoll = uint64(nanotime())

    // 设置 GOMAXPROCS。
    procs := ncpu
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n
    }
    if procresize(procs) != nil {
        throw("unknown runnable goroutine during bootstrap")
    }
}

与前一版本最大的区别是取消了 GOMAXPROCS 256 上限。

在完成核心初始化操作后,创建并执行 main goroutine,也就是 runtime.main 函数。

不过呢,有个概念要分清楚,前文所说初始化属于运行时内核层面。还有一种初始化属于逻辑层面,包括 runtime 包里的 init 函数,以及标准库、用户、第三方包初始化函数。不要看轻逻辑初始化执行方式,这可能关系到代码依赖,同步处理等等问题。

不要惊讶于栈(stack)可以有 1 GB 大小,这与 goroutine 实现方式有关。

说实话,我个人觉着这里面的一些内容应该放到 schedinit 里。

proc.go

// The main goroutine.
func main() {

    // 栈最大值。(64 位系统下 1GB)
    if sys.PtrSize == 8 {
        maxstacksize = 1000000000
    } else {
        maxstacksize = 250000000
    }

    // 启动后台监控。
    systemstack(func() {
        newm(sysmon, nil)
    })

    // 执行 runtime 包内的初始化函数。
    runtime_init()

    // 启动时间。
    runtimeInitTime = nanotime()

    // 启动垃圾回收器。
    gcenable()

    // 执行用户、标准库、第三方库初始化函数。
    fn := main_init
    fn()

    // 如果是库方式,则不执行用户入口函数。
    if isarchive || islibrary {
        // A program compiled with -buildmode=c-archive or c-shared
        // has a main, but it is not executed.
        return
    }

    // 执行用户入口函数。
    fn = main_main
    fn()

    // 退出进程。
    exit(0)
}

很显然,目标是 runtime.init、main.init,以及 main.main 这个用户入口函数。

//go:linkname runtime_init runtime.init
func runtime_init()
//go:linkname main_init main.init
func main_init()
//go:linkname main_main main.main
func main_main()

这两个初始化函数由编译器动态生成,所以需要写个示例来看看具体情形。

test.go

package main
import (
    "fmt"
    "runtime"
    _ "net/http"                    // 标准库
    _ "github.com/shirou/gopsutil/cpu"        // 第三方库
)
func init() {                    // 自定义
    println("user init1.")
}
func init() {
    println("user init2.")
}
func main() {
    fmt.Println(runtime.NumCPU())
}

编译调试版本,应禁用内联(inlining)和优化(optimizations)。

$ go build -gcflags "-N -l" -o test

用自带工具反汇编,输出编译器如何处理 init 初始化函数。这些函数都被重新命名,这也是同一包,甚至同一文件中允许有多个同名初始化函数的缘故。

$ go tool objdump test | grep "TEXT runtime\.init"
TEXT runtime.init.0(SB)  /usr/local/go/src/runtime/cpuflags_amd64.go
TEXT runtime.init.1(SB)  /usr/local/go/src/runtime/mgcwork.go
TEXT runtime.init.2(SB)  /usr/local/go/src/runtime/mstats.go
TEXT runtime.init.3(SB)  /usr/local/go/src/runtime/panic.go
TEXT runtime.init.4(SB)  /usr/local/go/src/runtime/proc.go
TEXT runtime.init.5(SB)  /usr/local/go/src/runtime/signal_unix.go
TEXT runtime.init(SB)    <autogenerated>
$ go tool objdump test | grep "TEXT main\.init"
TEXT main.init.0(SB)  /yuhen/go/src/test/test.go
TEXT main.init.1(SB)  /yuhen/go/src/test/test.go
TEXT main.init(SB)    <autogenerated>

接下来,反汇编 runtime.init 和 main.init。可以看出所有初始化函数都由它们完成调用。

$ go tool objdump -s "runtime\.init" test | grep "CALL.*init"

  <autogenerated>      CALL runtime.init.0(SB)         
  <autogenerated>      CALL runtime.init.1(SB)         
  <autogenerated>      CALL runtime.init.2(SB)         
  <autogenerated>      CALL runtime.init.3(SB)         
  <autogenerated>      CALL runtime.init.4(SB)         
  <autogenerated>      CALL runtime.init.5(SB)
$ go tool objdump -s "main\.init" test | grep "CALL.*init"

  <autogenerated>      CALL fmt.init(SB)
  <autogenerated>      CALL net/http.init(SB)              
  <autogenerated>      CALL github.com/shirou/gopsutil/cpu.init(SB)    
  <autogenerated>      CALL main.init.0(SB)                
  <autogenerated>      CALL main.init.1(SB)

基于输出结果看,runtime.init 仅负责 runtime 包,main.init 则包含标准库、第三方库,以及用户自定义函数。另外,所有初始化逻辑都在 main goroutine 内执行。待这些初始化全部完成后,再调用 main.main 函数,转入用户逻辑。

不管编译器实现算法是否对 init 函数进行排序,我们都不应该让多个初始化逻辑依赖某种特定次序。这极易造成混乱,引发潜在错误。正确做法是,初始化函数仅负责当前包,甚至仅仅是当前文件的逻辑。

快速导览:


分享给小伙伴们:
本文标签: Go语言

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号