Go Runtime 介绍

AI 摘要: runtime包提供了与运行时系统交互的函数和变量,环境变量GOGC用于设置垃圾回收触发比例,GOMEMLIMIT用于设置内存限制,GODEBUG用于设置调试参数,GOMAXPROCS用于设置操作系统线程数限制,GORACE用于检测数据竞争问题。

runtime 包

runtime包,https://pkg.go.dev/runtime#pkg-overview,包含了和 Go’s runtime system 交互的接口,诸如 Go 协程控制函数、底层等信息

环境变量

GOGC: 控制 GC 百分比,可以通过SetGCPercent(percent int)控制

A collection is triggered when the ratio of freshly allocated data to live data remaining after the previous collection reaches this percentage, The default is GOGC=100. Setting GOGC=off,

GOMEMLIMIT:限定 runtime 的内存限制,单位是字节,默认大小是math.MaxInt64,可以通过SetMemoryLimit(limit int64) 控制

This memory limit includes the Go heap and all other memory managed by the runtime, and excludes external memory sources such as mappings of the binary itself, memory managed in other languages, and memory held by the operating system on behalf of the Go program. GOMEMLIMIT is a numeric value in bytes with an optional unit suffix. The supported suffixes include B, KiB, MiB, GiB, and TiB The default setting is math.MaxInt64

GODEBUG:控制 debug 变量 更多参考说明: https://pkg.go.dev/runtime#pkg-overview

It is a comma-separated list of name=val pairs setting these named variables

allocfreetrace=1:内存分配跟踪

gcstoptheworld=1|2: gc STW 开关

gcstoptheworld: setting gcstoptheworld=1 disables concurrent garbage collection, making every garbage collection a stop-the-world event. Setting gcstoptheworld=2 also disables concurrent sweeping after the garbage collection finishes.

gctrace=1: gc 跟踪

gctrace: setting gctrace=1 causes the garbage collector to emit a single line to standard error at each collection, summarizing the amount of memory collected and the length of the pause. The format of this line is subject to change.

GOMAXPROCS:限制同时执行用户级 Go 代码的操作系统线程数,并不限定被 Go 阻塞的系统调用,可以通过 GOMAXPROCS()设定

The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit. This package’s GOMAXPROCS function queries and changes the limit.

GORACE: 数据竟态问题 参考: https://go.dev/doc/articles/race_detector

数据争用是并发系统中最常见和最难调试的错误类型之一,数据竟态会导致程序崩溃和内存错误 当两个 goroutine 同时访问同一变量并且至少其中一个访问是写入时,就会发生数据争用。有关详细信息,请参阅 Go 内存模型。

A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write. See the The Go Memory Model for details.

GOARCH, GOOS, GOPATH, GOROOT : Go 环境变量配置

The GOARCH, GOOS, GOPATH, and GOROOT environment variables complete the set of Go environment variables. They influence the building of Go programs (see https://golang.org/cmd/go and https://golang.org/pkg/go/build). GOARCH, GOOS, and GOROOT are recorded at compile time and made available by constants or functions in this package, but they do not influence the execution of the run-time system

数据竟态问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 两个协程分别对map进行写操作,map非并发写安全,可能导致竟态问题
func main() {
	c := make(chan bool)
	m := make(map[string]string)
	go func() {
		m["1"] = "a" // First conflicting access.
		c <- true
	}()
	m["2"] = "b" // Second conflicting access.
	<-c
	for k, v := range m {
		fmt.Println(k, v)
	}
}

// 可以通过go test -race 是否存在竟态问题
$ go test -race ./racex/...
==================
WARNING: DATA RACE
Write at 0x00c00009e3c0 by goroutine 8:
  runtime.mapaccess2_faststr()
      /usr/local/go/src/runtime/map_faststr.go:108 +0x43c
  x-learn/runtimex/pprofx/racex.TestMapRace.func1()
      /private/data/x-learn/go-refs/runtimex/pprofx/racex/map_racex_test.go:12 +0x4c
...

// 也可以将竟态错误log到指定文件
$ GORACE="log_path=/tmp/race/report strip_path_prefix=/my/go/sources/" go test -race

// 如果不想某些文件加入竟态检测,直接排除掉
// +build !race

package foo
...

典型数据竟态 Case

参考: https://go.dev/doc/articles/race_detector

  1. 循环内并发竟态计数 - 解决: 协程调用函数/方法时候,通过传参解决,go func(i)
  2. 意外共享变量 - 解决: 控制变量的作用域,在协程内通过重新赋值,避免和其他协程变量共享
  3. 未保护全局变量读写(通常是 map 并发安全问题) - 解决: 全局变量通过加锁保护
  4. 一些原子类型(通常是在结构体内变量并发读写) - 解决: 加锁、chan、sync/atomic 无锁
  5. 向关闭通道发生数据 - 解决: 确保数据发生完后再关闭通道

Race on loop counter - 循环内并发竟态计数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// bad - typically prints 55555, not 01234
func main() {
	var wg sync.WaitGroup
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func() {
			fmt.Println(i) // Not the 'i' you are looking for.
			wg.Done()
		}()
	}
	wg.Wait()
}

// good
func main() {
	var wg sync.WaitGroup
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func(j int) {
			fmt.Println(j) // Good. Read local copy of the loop counter.
			wg.Done()
		}(i)
	}
	wg.Wait()
}

Accidentally shared variable - 意外共享变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// bad
// ParallelWrite writes data to file1 and file2, returns the errors.
func ParallelWrite(data []byte) chan error {
	res := make(chan error, 2)
	f1, err := os.Create("file1")
	if err != nil {
		res <- err // 可能写入go协程新的err
	} else {
		go func() {
			// This err is shared with the main goroutine,
			// so the write races with the write below.
			_, err = f1.Write(data) // 和主协程有
			res <- err
			f1.Close()
		}()
	}
	f2, err := os.Create("file2") // The second conflicting write to err.
	if err != nil {
		res <- err
	} else {
		go func() {
			_, err = f2.Write(data) // 同理
			res <- err
			f2.Close()
		}()
	}
	return res
}

// 竟态检测
$ go test -race ./racex/common_race_err*
PASS
==================
WARNING: DATA RACE
Write at 0x00c0001124f0 by goroutine 8:
  command-line-arguments.ParallelWrite.func1()
      /private/data/x-learn/go-refs/runtimex/pprofx/racex/common_race_err.go:16 +0x80
...

// good
...
_, err := f1.Write(data)
...
_, err := f2.Write(data)
...

Unprotected global variable - 未保护的全局变量

当下面的 RegisterService 和 LookupService 函数在多个不同的协程中,并发读写 Map 不安全

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// bad
// Concurrent reads and writes of the same map are not safe
var service map[string]net.Addr

func RegisterService(name string, addr net.Addr) {
	service[name] = addr
}

func LookupService(name string) net.Addr {
	return service[name]
}

// good
var (
	service   map[string]net.Addr
	serviceMu sync.Mutex
)

func RegisterService(name string, addr net.Addr) {
	serviceMu.Lock()
	defer serviceMu.Unlock()
	service[name] = addr
}

func LookupService(name string) net.Addr {
	serviceMu.Lock()
	defer serviceMu.Unlock()
	return service[name]
}

Primitive unprotected variable - 结构体内的原始变量(bool,int,int64 etc.)也可能被竟态

解决: 可以通过锁、chan、sync/atomic(无锁化)包处理该类竟态问题

A typical fix for this race is to use a channel or a mutex. To preserve the lock-free behavior, one can also use the sync/atomic package.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// bad
type Watchdog struct{ last int64 }

func (w *Watchdog) KeepAlive() {
	w.last = time.Now().UnixNano() // First conflicting access.
}

func (w *Watchdog) Start() {
	go func() {
		for {
			time.Sleep(time.Second)
			// Second conflicting access.
			if w.last < time.Now().Add(-10*time.Second).UnixNano() {
				fmt.Println("No keepalives for 10 seconds. Dying.")
				os.Exit(1)
			}
		}
	}()
}

// ok
// 通过sync/atomic包无锁
type Watchdog struct{ last int64 }

func (w *Watchdog) KeepAlive() {
	atomic.StoreInt64(&w.last, time.Now().UnixNano())
}

func (w *Watchdog) Start() {
	go func() {
		for {
			time.Sleep(time.Second)
			if atomic.LoadInt64(&w.last) < time.Now().Add(-10*time.Second).UnixNano() {
				fmt.Println("No keepalives for 10 seconds. Dying.")
				os.Exit(1)
			}
		}
	}()
}

Unsynchronized send and close operations - 未同步发送和关闭操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 这里存在的问题是,可能向已关闭的通道发生消息
c := make(chan struct{}) // or buffered channel

// The race detector cannot derive the happens before relation
// for the following send and close operations. These two operations
// are unsynchronized and happen concurrently.
go func() { c <- struct{}{} }()
close(c)

// 也可以检测出来
$ go test -race ./racex/common_race_err_test.go
PASS
==================
WARNING: DATA RACE
Read at 0x00c000090310 by goroutine 9:
  runtime.chansend1()
      /usr/local/go/src/runtime/chan.go:145 +0x2c
  command-line-arguments.TestChanRace.func1()
      /private/data/x-learn/go-refs/runtimex/pprofx/racex/common_race_err_test.go:14 +0x34
...


// good - 根据Go内存模型,消息在chan上的发送一定会在接受之前完成,要同步发送和关闭操作,保证在关闭之前完成发送的接收操作
c := make(chan struct{}) // or buffered channel

go func() { c <- struct{}{} }()
<-c
close(c)

race 探测器会增加执行时间和内存

注意:如果是长期运行的程序,持续的竟态探测可能带来内存泄露(defer 和 recover 作用),可以通过runtime.ReadMemStats观察