这是我参与「第五届青训营」伴学笔记创作活动的第 5 天
Go语言最大的特征就是它强大的并发能力,Go语言之所以拥有如此强大的并发能力,离不开Go语言的协程,Go语言的协程能够提供强大的并发能力。
它之所以拥有这样强大的能力我们和传统的Web后端语言Java对比来看。
| 线程 | 协程 |
|---|---|
| 线程由操作系统内核管理,并且拥有硬件依赖性 | Go协程由go的运行时管理,没有硬件依赖性 |
| 操作系统线程通常是1-2MB固定大小的堆栈 | Go协程一般是8KB的堆栈大小,在1.14变成了2KB |
| 堆栈大小由编译时确定,不能动态增加 | Go协程中的堆栈在运行时有需要会动态拓展,最大能拓展到1GB,可以通过动态释放和分配堆内存来实现 |
| 线程之间没有简便的通信方式,线程之间通讯延迟很高 | Go协程通过通道Channels来保持低延迟通信 |
| 线程之间有身份,可以通过线程ID来获取不同线程的身份,Java可以获取线程名以及给线程命名 | Go协程没有任何身份, go这样实现是因为Go没有线程本地存储[Thread LocalStorage]。 |
| 线程的启动和销毁都需要调用大量的系统资源并且完成以后才能返回 | Go协程的创建和销毁开销很小,操作系统不直接调用协程,由用户自行调用 |
| 线程是抢占调度的,线程之间切换成本比较高,因为调度程序需要保存/恢复50个以上的寄存器和状态。当多个线程频繁切换时,这个开销成本很高。 | Go是协作调度的,当进行切换时,仅需保存或恢复3个寄存器。 |
1.main结束所有的goroutine都会直接结束 下面这串代码,如果我们点击运行,理论上基本不会出现打印的信息,因为当执行到第2行会开启一个goroutine, 然后在开启goroutine的时候main函数里的程序会继续往下运行,之后就没有任何代码,所以main函数直接就结束,当main函数直接结束,会立刻销毁结束所有的goroutine,也就是说,打印数据的这段代码还没正式开始运行就被结束了。
func main() {
go fun(1)
}
func fun(num int) {
for i := 0; i < 100; i++ {
fmt.Printf("线程%v:%v\n", num, i)
}
}
那么问题来了,是不是意味着父goroutine结束的同时也会结束所有的子goroutine?
func main() {
go father()
select {}
}
func father() {
go son(1)
go son(2)
}
func son(num int) {
for i := 0; i < 10; i++ {
fmt.Printf("线程%v:%v\n", num, i)
}
}
以上代码通过select阻塞main函数来防止main函数结束运行,然后运行father,之后father又运行两个子协程,当7,8行结束以后仍然没有可以正常打印son协程的信息,说明父goroutine结束,子goroutine并不会结束。所以可以看出,只有main函数是特殊的。