Go你必须知道的知识

89 阅读6分钟

有关Goroutine无限创建的分析

1.从操作系统分析进程、线程、协程的区别

进程、线程、协程都是因为并发而生,但是它们各自使用范围又是完全不同的。

1.1 进程内存

进程是与一个可执行程序在运行中而形成的一个独立的内存体,这个内存体有独立的地址空间,linux操作系统会给每个进程分配一个虚拟内存空间,其中32位操作系统为4GB,64位操作系统则会更大。进程有自己的堆空间,进程直接被操作系统调度。操作系统实际也是以进程为单位,分配系统资源,如cpu时间片、内存等资源,所以以此特点划分进程被称作操作系统资源分配的最小单位。

1.2 线程内存

线程可以被称作轻量级进程(LWP),是cpu调度执行的最小单位。

线程特性:

​ 多个线程共同“寄生”在同一个进程上,这些线程都拥有各自的栈空间。但其他内存空间和其他线程共同共享。

​ 线程之间的内存关联很大,但互相通信却很简单,堆区、全局区等数据共享。只需要加锁机制便可完成同步通信,这种特征同时也让线程之间关联性较大,如一个线程出问题,则会导致进程也出问题,从而导致其他线程也出问题。

1.3 执行单元

对于linux操作系统来讲,并不会去区分即将执行的单元是进程还是线程,进程和线程都是一个单位的执行单元,CPU会一视同仁,平均分配时间片。

开发者可以通过一个进程提高内部线程的数量从而增加被cpu分配到时间比例,多开一些线程能够提高进程运行效率,这样能让固定的cpu资源更多的分配到自己程序上。

是不是线程可以无限多呢?》当然不是。

切换内核和切换硬件上下文都会触发性能的开销,切换时会保存寄存器中的内容,讲之前的执行流程状态保存,也会导致CPU高速缓存失效。

两个切换可以理解为它所带来的后果和影响时由于页表查找是一个很慢的过程,因此同城使用Cache来缓存常用的地址映射,这样可以加速也表查询,Cache失效导致命中率降低,因此虚拟地址转换为物理地址就会变慢,表现出来就是程序变慢。

优化措施:

​ 让CPU内核不切换执行单元,而在用户态切换执行流程。

开发者没有权限修改操作系统的内核机制,因此只能在用户态再创建一个伪执行单元,这就是线程。

2.协程切换成本

协程切换对比线程:

1.协程完全在用户空间进行线程切换,涉及特权模式切换,需要在内核空间完成。

2.协程切换相比线程切换做的事情更少,线程需要有内核和用户态的切换,以及系统调用过程。

2.1 协程切换成本

协程切换非常简单,就是把当前协程的cpu寄存器的状态保存起来,然后将需要切换进来的协程cpu寄存器状态家在到cpu的寄存器上就可以了,而且完全在用户态进行,一般来讲一次协程上下文切换最多只需要几十纳秒的时间。

2.2 线程切换成本

系统内核调度的对象是线程,因为线程是调度单元的基本单位(进程是资源拥有的最基本单元,进程的切换需要做的事情更多,这里暂时不考虑进程切换),而线程的调度只是拥有最高权限的内核空间才可以完成,所以线程的切换设计用户空间和内核空间的切换,也就是特权模式切换,然后需要操作系统的调度模块完成线程调度,除了和协程基本相同的CPU上下文,还有线程私有的栈和寄存器等,上下文协程多一些。

3. Go是否可以无限创建,如何限定数量

1.不控制Goroutine数量引发的问题

如果做一个服务器或者一些高业务的场景能否随意开辟Goroutine并且不去主动回收呢?能够通过强大的GC和优质的调度算法来支撑呢?

package main

import (
	"fmt"
	"math"
	"runtime"
)

func main() {
	task_cnt := math.MaxInt64

	for i := 0; i < task_cnt; i++ {
		go func(i int) {
			fmt.Println("go func ", i, " goroutine count =", runtime.NumGoroutine())
		}(i)
	}
}

result

go func  92596  goroutine count = 90600
panic: too many concurrent operations on a single file or socket (max 1048575)

goroutine 1140868 [running]:
internal/poll.(*fdMutex).rwlock(0x14000128060, 0x14?)
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/internal/poll/fd_mutex.go:147 +0x13c
internal/poll.(*FD).writeLock(...)
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/internal/poll/fd_mutex.go:239
internal/poll.(*FD).Write(0x14000128060, {0x1412b88e500, 0x2c, 0x40})
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/internal/poll/fd_unix.go:370 +0x48
os.(*File).write(...)
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/os/file_posix.go:48
os.(*File).Write(0x14000126008, {0x1412b88e500?, 0x2c, 0x10248c408?})
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/os/file.go:176 +0x64
fmt.Fprintln({0x1024c41f8, 0x14000126008}, {0x140032c4768, 0x4, 0x4})
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/fmt/print.go:265 +0x78
fmt.Println(...)
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/fmt/print.go:274
main.main.func1(0x0?)
        /Users/shanzhudaheng/ownProject/project/test/main.go:14 +0x11c
created by main.main
        /Users/shanzhudaheng/ownProject/project/test/main.go:13 +0x58
panic: too many concurrent operations on a single file or socket (max 1048575)

短时间内占据操作系统的资源(CPU、内存、文件描述等)

cpu使用率不断上升、内存占用不断上涨、主进程崩溃。

2.一些简单的方法控制Goroutine的数量

package main

import (
	"fmt"
	"math"
	"runtime"
	"sync"
)

var wg = sync.WaitGroup{}

func busy(ch chan bool, i int) {
	fmt.Println("go func ", i, " groutine count = ", runtime.NumGoroutine())
	<-ch
	wg.Done()
}

func main() {
	task_cnt := math.MaxInt64

	ch := make(chan bool, 3)

	for i := 0; i < task_cnt; i++ {
		wg.Add(1)
		ch <- true
		go busy(ch, i)

	}
	wg.Wait()
}

3.推荐chan处理(go work池处理)

package main

import (
	"fmt"
	"math"
	"runtime"
	"sync"
)

//同步锁
var wg = sync.WaitGroup{}

func busy(ch chan int) {
	for t := range ch {
		fmt.Println(" go task = ", t, ", goroutine count =", runtime.NumGoroutine())
		wg.Done()
	}
}

//发送消息
func sendTask(task int, ch chan int) {
	wg.Add(1)
	ch <- task
}

func main() {
	ch := make(chan int)

	go_cnt := 3

	for i := 0; i < go_cnt; i++ {
		go busy(ch)
	}

	task_cnt := math.MaxInt64

	for i := 0; i < task_cnt; i++ {
		sendTask(i, ch)
	}
	wg.Wait()
}