今天主要学习了一下go语言的多线程,也写了一些例子,最开始还是很困惑。
比如下面这个例子:
package main
import "fmt"
func loop() {
for i := 0; i < 10; i++ {
fmt.Printf("%d\n", i)
}
}
func main() {
go loop()
}
都说go语言的多线程语法简单,只需要go+一个函数名称就可以,可是我执行上述代码以后却没有任何输出。很郁闷。
然后就查了很多关于go语言多线程的知识。
重点比如这篇:javascript:void(0)
go的多线程准确是协程,goroutine。协程是比线程更轻量级的概念。这个具体体现在线程一般是内核支持的,需要系统调用和时钟中断来支持,吃内存费时间。所以,一个程序中如果有大量的线程,那么系统很可能吃不消。而协程则是运行在用户态的,具体是运行在某一个线程中,协程由用户态的调度器完成调度。所以相比较于java中的线程,每一次启动都需要切换到内核态,而且线程本身需要吃掉比较多的资源,协程就不需要切换内核态,而且需要的资源也很少,基本上只有2KB左右。这估计是go适合服务端开发的原因。
实际上,go内部有一个runtime层,里面实现了线程,协程以及调度等模块。go+func的语言用于开始一个协程或者goroutine,goroutine所对应的数据结构会保存其运行时的各种状态比如堆栈等动态信息。然后会有一个goroutine的队列,存放提交的goroutine。runtime的调度器会周期性地从队列中取gorouitne来执行。有一个类似线程池的东西会来执行每一个goroutine,如果有闲置的线程就会唤醒这个线程,否则如果全部的线程都在工作,就开始一个新的线程。runtime中有一个线程会监事每一个goroutine或者线程的执行,如果线程阻塞了,那么该线程就会被挂起,直到其他的goroutine唤醒它,如果一个goroutine的线程运行的太久,那么该线程会被剥夺来运行其他的goroutine。调度是抢占式的。所以创建协程的代价很低,而且可以用之前的线程来跑,效率比纯线程好。
具体过程在这本书里面很详细:
https://github.com/qyuhen/book/blob/master/Go%20学习笔记%20第四版.pdf
明白了go语言协程的大致模型,在最开始运行的时候,至少有两个goroutine一个是main的,一个是idle的,main的goroutine是主goroutine,只要主goroutine结束了,那么其他由main产生的goroutine也会被迫停止运行。
上面的例子就很好理解了。因为在一个函数中使用go开启一个协程以后,该函数是不会做任何等待的,而是直接执行本函数的下一条语句,所以这时main函数执行go loop()以后,直接向下执行,最后main函数直接就返回了,主协程结束,其他协程也结束,没有给loop协程运行的机会。
那么这里如果要等loop运行该怎么办?
一是可以在main的最后通过time的sleep函数等一下。二是通过channel机制,在main的最后调用一个read来读取loop写入的一个值,那么只要loop没有写入,main就会read阻塞在那知道loop写入。
package main
import "fmt"
var ch chan int = make(chan int)
func read() {
fmt.Printf("%d", <-ch)
}
func write() {
ch <- 1
}
func main() {
go read()
write()
}
但是以上这段代码也没有输出。channel只有两个操作,那么写入要么读取,这两个都是阻塞的,或者成对的,只要缺一个,另外一个都会阻塞。所以,才有了最开始使用main函数最后放一个read的方式来实现类似join的操作。
但是上面这个例子只是颠倒了read和write的顺序,我觉得也应该有输出才对。仔细想了一下,原因应该是,延迟问题。write执行以后,会等read函数来读取,否则write不会返回,当read的协程执行了读取操作以后,此时write就返回了,main就结束了,但是fmt还没有把channel里的数据打印出来,也就是延迟。所以main里面先写read肯定是没问题的,因为肯定是先写后读。
这个例子只要在main后面加一个sleep就会有输出了。