Golang 中goroutine的理解
CSP模型
Golang中通过CSP(communicating sequential processes)模型来通信,不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。 CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。Golang中channel 是被单独创建并且可以在进程之间传递,它的通信模式类似于 boss-worker 模式的,一个实体通过将消息发送到channel 中,然后又监听这个 channel 的实体处理,两个实体之间是匿名的,这个就实现实体中间的解耦,其中 channel 是同步的一个消息被发送到 channel 中,最终是一定要被另外的实体消费掉的,在实现原理上其实类似一个阻塞的消息队列。
Goroutine 是Golang实际并发执行的实体,它底层是使用协程(coroutine)实现并发,coroutine是一种运行在用户态的用户线程,Golang底层选择使用coroutine的出发点是因为,
它具有以下特点:
- 用户空间 避免了内核态和用户态的切换导致的成本.
- 可以由语言和框架层进行调度.
- 更小的栈空间允许创建大量的实例.
Golang 调度器GPM原理与调度分许
Golang 中正确理解context
在完全理解goroutine前可以尝试先阅读这两篇文章
golang在语言层面支持协程,goroutine的切换和管理不再依赖于系统的线程与进程,也不依赖于cpu核心数量,由golang运行时统一管理。在一些其他语言中,主要通过库的方式支持协程,仅支持一些简单的(创建,销毁,切换等功能),在这种方式生成的协程中调用一个同步 IO 操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行的协程,从而无法达到轻量级线程本身期望达到的目标
goroutine的生命周期:
goroutine 以非阻塞的方式执行,它们会随着程序(主线程)的结束而消亡,它们的生命周期取决于创建它们的线程,goroutine之间是不存在父子关系,在goroutine中创建新的goroutine,父goroutine结束不影响子roroutine继续运行,他们应该同属于某一个线程
父子协程的生命周期问题:
func main(){ print("main start\n") go func() { print("父goroutine start\n") go func() { for { print("子goroutine start\n") timer := time.NewTimer(time.Second * 1) <-timer.C } }() time.Sleep(time.Second*2) print("父goroutine finish\n") }() time.Sleep(time.Second*5) print("main finish\n") }
main start
父goroutine start
子goroutine start
子goroutine start
父goroutine finish
子goroutine start
子goroutine start
子goroutine start
main finish
输出结果显示子goroutine是不受父goroutine控制的
如果想通过父goroutine控制子goroutine,那么就需要context
func main(){ print("main start\n") go func() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() print("父goroutine start\n") go func() { for { select { case <-ctx.Done(): print("子goroutine have to finish\n") return default: print("子goroutine start\n") timer := time.NewTimer(time.Second * 1) <-timer.C } } }() time.Sleep(time.Second*2) print("父goroutine finish\n") }() time.Sleep(time.Second*5) print("main finish\n") }
输出
main start
父goroutine start
子goroutine start
子goroutine start
父goroutine finish
子goroutine have to finish
main finish
goroutine与线程的区别
从系统调度的角度,线程由系统内核去调度,每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用内核调度函数,终止正在运行的线程,把该线程的寄存器信息保存到内存当中,再从内存恢复某一个线程的注册表信息,继续执行选中的线程。这种OS级别的线程切换需要完整的上下文切换过程,即保存一个线程的状态到内存中,再从内存中恢复另一个线程的状态,再更新调度器的数据结构,这个过程相对耗时。goroutine的切换由processor管理,不需要由时钟中断来控制,也不用切换到内核语境,开销非常小
从栈空间来看,系统线程分配固定的2MB栈空间,用于存储函数调用期间,其他函数的局部变量,goroutine栈空间分配2KB,其可以按需增大和缩小,最大限制可以到1GB
使用goroutine要注意防止内存泄漏
goroutine 泄露发生时,该 goroutine 的栈(一般 2k 内存空间起)一直被占用不能释放,goroutine 里的函数在堆上申请的空间也不能被 垃圾回收器 回收。这样,在程序运行期间,内存占用持续升高,可用内存越来也少,最终将导致系统崩溃。
常见的泄漏场景有往channel写没有读,导致阻塞(channel还有很多中阻塞的情况),select中阻塞,for循环不退出
可以通过一些代码习惯避免这些泄漏,比如select,可以用定时器解决阻塞,也可以用context解决阻塞
func fibonacci(c chan int, ctx context.Context) { x, y := 0, 1 for{ select { case c <- x: x, y = y, x+y case <-ctx.Done(): fmt.Println("quit") return } } } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := make(chan int) go fibonacci(c, ctx) for i := 0; i < 10; i++{ fmt.Println(<- c) } cancel() time.Sleep(5 * time.Second) }
本文来自博客园,作者:LeeJuly,转载请注明原文链接:https://www.cnblogs.com/peterleee/p/11874618.html