协程(Coroutine)
本质上是一种用户态线程,不需要操作系统来进行抢占式调度, 且在真正的实现中寄存于线程中,因此,系统开销极小,可以有效提高线程的任务并发 性,而避免多线程的缺点。使用协程的优点是编程简单,结构清晰;缺点是需要语言的 支持,如果不支持,则需要用户在程序中自行实现调度器。
执行体是个抽象的概念,在操作系统层面有多个概念与之对应,比如操作系统自己掌管的 进程(process)、进程内的线程(thread)以及进程内的协程(coroutine,也叫轻量级线程)。与 传统的系统级线程和进程相比,协程的大优势在于其“轻量级”,可以轻松创建上百万个而不 会导致系统资源衰竭,而线程和进程通常多也不能超过1万个。这也是协程也叫轻量级线程的 原因。
多数语言在语法层面并不直接支持协程,而是通过库的方式支持,但用库的方式支持的功能 也并不完整,比如仅仅提供轻量级线程的创建、销毁与切换等能力。如果在这样的轻量级线程中 调用一个同步 IO 操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行轻量级线程, 从而无法真正达到轻量级线程本身期望达到的目标。
Go 语言在语言级别支持轻量级线程,叫goroutine。Go 语言标准库提供的所有系统调用操作 (当然也包括所有同步 IO 操作),都会出让 CPU 给其他goroutine。这让事情变得非常简单,让轻 量级线程的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量。
goroutine是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。你将会发现,它的 使用出人意料得简单。 假设我们需要实现一个函数Add(),它把两个参数相加,并将结果打印到屏幕上,具体代码 如下:
func Add(x, y int) {
z := x + y
fmt.Println(z)
}
那么,如何让这个函数并发执行呢?具体代码如下:
go Add(1, 1)
如果没有在代码中通过runtime.GOMAXPROCS(n) (其中n是整数)指定使用多核的话,goroutins都是在一个线程里的,它们之间通过不停的让出时间片轮流运行,达到类似同时运行的效果。
当一个goroutine发生阻塞,Go会自动地把与该goroutine处于同一系统线程的其他goroutines转移到另一个系统线程上去,以使这些goroutines不阻塞。
runtime.Gosched()
untime.Gosched()用于让出CPU时间片。这就像跑接力赛,A跑了一会碰到代码runtime.Gosched()就把接力棒交给B了,A歇着了,B继续跑。
看代码:
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 2; i++ {
runtime.Gosched()
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
输出结果:
hello
world
hello
注意结果:
1、先输出了hello,后输出了world.
2、hello输出了2个,world输出了1个(因为第2个hello输出完,主线程就退出了,第2个world没机会了)
把代码中的runtime.Gosched()注释掉,执行结果是:
hello
hello
因为say("hello")这句占用了时间,等它执行完,线程也结束了,say("world")就没有机会了。
这里同时可以看出,go中的goroutins并不是同时在运行。事实上,如果没有在代码中通过
runtime.GOMAXPROCS(n) 其中n是整数,
指定使用多核的话,goroutins都是在一个线程里的,它们之间通过不停的让出时间片轮流运行,达到类似同时运行的效果。
还需要学习一句话:
当一个goroutine发生阻塞,Go会自动地把与该goroutine处于同一系统线程的其他goroutines转移到另一个系统线程上去,以使这些goroutines不阻塞