Go 协程
Go 协程
1.进程和线程
-
进程是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。
-
线程是进程的一个执行实例,是程序执行的最小单位,是比进程更小的能单独执行的基本单位。
-
一个进程可以创建和销毁多个线程,同一个进程中的线程可以并发执行。
-
一个程序至少有一个进程,一个进程至少有一个线程。
并发和并行:
(1)并发:多线程程序在单核上运行
- 多个任务在一个cpu
- 从微观角度上看,在一个时间点,实际上只有一个任务在执行
(2)并行:多线程程序在多核上运行
- 多个任务作用在多个cpu
- 从微观角度上看,在一个时间点,有多个任务在执行,这样来看,并行速度更快
2.Go协程和Go主线程
在一个Go线程上,可以有多个协程,可以把协程理解为轻量级的线程(编译器优化)。
Go协程特点:
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
【案例】
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("test:", i)
time.Sleep(time.Second)
}
}
func main() {
go test() //开启一个协程
for i := 1; i <= 10; i++ {
fmt.Println("main:", i)
time.Sleep(time.Second)
}
}
这里main是一个主线程,go test()
则是开启了一个协程,下面是输出结果
test: 1
main: 1
main: 2
test: 2
main: 3
test: 3
test: 4
main: 4
main: 5
test: 5
test: 6
main: 6
main: 7
test: 7
test: 8
main: 8
main: 9
test: 9
test: 10
main: 10
可见主线程和协程同时执行。
把test()中的sleep时间乘2,再进行测试,结果如下:
main: 1
test: 1
main: 2
test: 2
main: 3
main: 4
test: 3
main: 5
main: 6
test: 4
main: 7
main: 8
test: 5
main: 9
main: 10
test: 6
很明显,在主线程执行结束后,协程也停止了运行。
总结:
协程就是线程的一部分,一个线程中可以创建多个协程。每个线程都有一个处理器,负责调度协程的执行。自然,当线程结束时,该线程内的所有协程也会被销毁。
MGP模式
M:主线程(Machine) G:协程(Goroutine) P:处理器,上下文环境 (Processor)
M下的P负责调度所有G,此时只有一个G正在运行,其他处于就绪态,等待P调度。
运行状态1:
- 当前程序有三个M,如果三个M都在一个CPU运行,就是并发,如果在不同的CPU运行,就是并行。
- M1 M2 M3都在执行一个G,协程队列(runqueue)有其他G等待。
- Go协程是轻量级的线程,是逻辑态的,可以容易的起上万个协程。
运行状态2:
- 图1:M0正在执行G协程,其他协程等待,G线程阻塞(如读取文件、访问数据库)。
- 图2:这时,就会创建M1线程(或者从线程池取出),并将等待的三个协程放到M1下执行。
- 当G不再阻塞,M0会被放到空闲的主线程等待唤醒。
设置Golang运行的cpu数
为了充分利用多cpu的优势,可以设置运行cpu的个数
func main() {
num := runtime.NumCPU()
runtime.GOMAXPROCS(num)
fmt.Println(num) //8
}
3.channel
3.1 问题
【案例】计算1-200各个数的阶乘
var (
myMap = make(map[int]int, 10)
)
func test(n int) {
ans := 1
for i := 1; i <= n; i++ {
ans *= i
}
myMap[n] = ans
}
func main() {
for i := 1; i <= 200; i++ {
go test(i)
}
//time.Sleep(time.Second*10)
fmt.Println(myMap)
}
//fatal error: concurrent map writes
问题:
- 主线程结束,但协程没有全部结束,使用
time.Sleep()
等待 - 不能同时向同一个内存空间(map)写,报错
concurrent map writes
使用全局变量加锁同步改进
var (
myMap = make(map[int]int, 10)
lock sync.Mutex
)
func test(n int) {
ans := 1
for i := 1; i <= n; i++ {
ans *= i
}
lock.Lock()
myMap[n] = ans
lock.Unlock()
}
func main() {
for i := 1; i <= 200; i++ {
go test(i)
}
time.Sleep(time.Second*10)
fmt.Println(myMap)
}
问题:
- 主线程等待所有协程完成的时间很难确定,这里设置了十秒,可能浪费了时间,也可能仍有协程处于工作状态。
- 通过全局变量锁实现通讯,不利于多个协程对全局变量的读写。
3.2 channel
(1)简介
- 本质上是一个队列,数据先进先出
- 本身线程安全,不需要加锁
- channel是有类型的,如string类型的channel只能存放string类型的数据
(2)定义与使用
var intChan chan int //存放int数据的channel
var mapChan chan map[int]string //存放map[int]string数据的channel
...
- channel是引用类型
- channel必须初始化,即make后才能使用
- channel有类型,只能写入定义类型的数据
测试案例:
func main() {
//创建channel
intChan := make(chan int, 3)
fmt.Printf("值:%v,地址:%p\n", intChan, &intChan)
//存值
intChan <- 10
intChan <- 20
a := 100
intChan <- a
//intChan <- 30 存值不能超过容量
fmt.Printf("值:%v,长度:%v,容量:%v\n", intChan, len(intChan), cap(intChan))
//取值
num1 := <-intChan
fmt.Println("num1=", num1)
fmt.Printf("值:%v,长度:%v,容量:%v\n", intChan, len(intChan), cap(intChan))
num2 := <-intChan
num3 := <-intChan
//num4 := <-intChan 数据取完继续取值会报错
fmt.Println(num2, num3)
}
/*
值:0xc00010e080,地址:0xc000006028
值:0xc00010e080,长度:3,容量:3
num1= 10
值:0xc00010e080,长度:2,容量:3
20 100
*/
allChan用法:
allChan可以存放任意类型变量
func main() {
allChan := make(chan interface{}, 10)
allChan <- 10
allChan <- 10.1
allChan <- "abc"
p := Person{
name: "zs",
}
allChan <- p
v1 := <-allChan
v2 := <-allChan
v3 := <-allChan
v4 := <-allChan
fmt.Println(v1, v2, v3, v4)
}
但是allChan不能调用对象的属性
allChan := make(chan interface{}, 10)
p := Person{
name: "zs",
}
allChan <- p
v1 := <-allChan
fmt.Println(v1.name)//这里报错
(3)channel遍历与关闭
- channel支持for-range遍历
- 在遍历时,如果channel没有关闭,则出现deadlock错误;已经关闭,则正常遍历
func main() {
intChan := make(chan int, 100)
for i := 0; i < 100; i++ {
intChan <- i
}
//fatal error: all goroutines are asleep - deadlock!
//close(intChan)
for v := range intChan {
fmt.Println(v)
}
}
【案例1】
-
声明一个管道intChan
-
开启一个writeData协程,向intChan写入数据;开启一个readData协程,从intChan读取数据;
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Println("write", i)
//time.Sleep(time.Second)
}
close(intChan)
}
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
fmt.Println("read", v)
//time.Sleep(time.Second)
}
exitChan <- true
close(exitChan)
}
func main() {
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)
go writeData(intChan)
go readData(intChan, exitChan)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
【案例2】
阻塞的简单案例,阻塞的定义和解决方法在后面的 3.3(2)阻塞和select 细说。
// 修改案例1中的代码
...
func main() {
intChan := make(chan int, 10)//50改为10
exitChan := make(chan bool, 1)
go writeData(intChan)
//go readData(intChan, exitChan)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
只写不读就可能出现阻塞,该案例中管道容量为10,存够十条数据后继续存就会报错。
【案例3】
统计1-8000中,哪些数字是素数?
传统的方法是直接循环,这里使用4个协程,缩短任务时间。
//存入数据
func putNum(intChan chan int) {
for i := 1; i <= 8000; i++ {
intChan <- i
}
close(intChan)
}
//取出并判断
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool
for {
num, ok := <-intChan
if !ok {
break
}
flag = true
for i := 2; i < num; i++ {
if num%i == 0 {
flag = false
break
}
}
if flag {
primeChan <- num
}
}
exitChan <- true
}
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000)
exitChan := make(chan bool, 4)
go putNum(intChan)
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//判断四个协程都已经结束
go func() {
for i := 0; i < 4; i++ {
<-exitChan
}
close(primeChan)
}()
for {
res, ok := <-primeChan
if !ok {
break
}
fmt.Println(res)
}
}
3.3使用细节和注意事项
(1)只读或只写
channel可以声明为只读或者只写
func main() {
//只写
chan1 := make(chan<- int,3)
chan1 <- 1
//只读
var chan2 <-chan int
num2 := <-chan2
}
【案例】
func send(ch chan<- int, exitchan chan struct{}) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
var a struct{}
exitchan <- a
}
func recv(ch <-chan int, exitchan chan struct{}) {
for {
v, ok := <-ch
if !ok {
break
}
fmt.Println(v)
}
var a struct{}
exitchan <- a
}
func main() {
ch := make(chan int, 10)
exitchan := make(chan struct{}, 2)
go send(ch, exitchan)
go recv(ch, exitchan)
var total = 0
for _ = range exitchan {
total++
if total == 2 {
break
}
}
}
(2)阻塞和select
a. 什么时候会发生阻塞?
-
向一个值为nil的管道写或读数据
-
无缓冲区时单独的写或读数据
-
缓冲区为空时进行读数据
-
缓冲区满时进行写数据
b. 解决阻塞的方法select-case
func main() {
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//在实际问题中,我们不知道什么时候关闭管道,可以用select解决
for {
select {
case v := <-intChan:
fmt.Printf("intChan读取数据:%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan:
fmt.Printf("stringChan读取数据:%s\n", v)
time.Sleep(time.Second)
default:
fmt.Println("全部取到!")
time.Sleep(time.Second)
return
}
}
}
如上面的代码,select对所有管道进行监控,当该管道未发生阻塞时则执行对应的代码。
此时,如果所有管道阻塞(如上述代码,所有管道数据都被取空),这时就需要用到default。当所有管道阻塞时,就会执行default中的代码。
但是,在实际生产中,当所有管道阻塞时,我们并不希望程序立刻结束,而是希望等待一段时间,这是我们就可以用到time.After。看下面的代码:
func putChan(intChan chan int) {
time.Sleep(time.Second * 3)
fmt.Println("intChan存入数据")
intChan <- 1
}
func main() {
intChan := make(chan int, 10)
go putChan(intChan)
for {
select {
case <-intChan:
fmt.Println("intChan取出数据")
default:
fmt.Println("程序结束")
return
}
}
}
/*
程序结束
*/
这里要实现从putChan()中存入数据,再从主线程中取出数据。但是putChan耽搁了3秒,导致主线程中的所有case阻塞,执行了default,结束了程序。
这里就可以使用time.After,等待一段时间,确保功能的实现。
func putChan(intChan chan int) {
time.Sleep(time.Second * 3)
fmt.Println("intChan存入数据")
intChan <- 1
}
func main() {
intChan := make(chan int, 10)
go putChan(intChan)
for {
select {
case <-intChan:
fmt.Println("intChan取出数据")
case <-time.After(time.Second * 5):
fmt.Println("timeout")
return
}
}
}
/*
intChan存入数据
intChan取出数据
timeout
*/
c. 阻塞的本质
深入详解Go的channel底层实现原理【图解】 - 云+社区 - 腾讯云 (tencent.com)
channel的整体结构(源码)
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
这里,主要说的是以下两个部分:
-
buf:缓存区,用来存放管道存放的内容,是个循环链表。如
intChan := make(chan int, 10)
,那么缓存区的大小就是10。 -
sendq和recvq: 分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表。主要用来存放遇到阻塞时,不能继续运行的协程。
接下来,用下面的案例,分析管道的阻塞与处理的方法。
func main() {
intChan := make(chan int, 3)
//管道的容量为3,同时向管道存入、取出数据
//可能出现:缓冲区为空时进行读数据、缓冲区满时进行写数据 两种情况
for {
select {
case <-intChan:
fmt.Println("取出数据")
time.Sleep(time.Second)
case intChan <- 1:
fmt.Println("存入数据")
time.Sleep(time.Second)
default:
return
}
}
}
情况1:缓冲区满时进行写数据
intChan <- 1
intChan <- 1
intChan <- 1
intChan <- 1
G0表示向buf内存储数据的线程,当G0执行三次后,buf已满,如下图。
G0想要继续向buf中存入数据,就会出现阻塞。这时,G0就会等待,让就绪中的G1运行。同时G0会被抽象成含有G0指针和send元素的sudog
结构体保存到sendq中等待被唤醒。
这时,如果G1执行了<-intChan
,buf就有了存储的空间。 channel会将等待队列中G0推出,将G0当时send的数据推到缓存中,然后G0唤醒,重新进入就绪状态。
情况2:缓冲区为空时进行读数据
a := <- intChan
buf为空,G0想要从buf中取出数据,会出现阻塞。这时,同情况1一样,G0就会等待,让就绪的G1运行。同时G0会被抽象成含有G0指针和recv元素的sudog
结构体保存到recvq中等待被唤醒。
G1运行了下面代码。
intChan <- 1
这时,与之前不同的情况出现了。G0不会像之前一样直接被推出唤醒。
而是从G1中取出数据,直接copy到G0的栈中(如下图)。使用这种方法, 在唤醒过程中,G0无需再获得channel的锁,然后从缓存中取数据。减少了内存的copy,提高了效率。
之后,G0才会被正常唤醒,重新排队。
(3)panic和recover
使用recover,解决协程中出现panic,导致程序崩溃的问题
panic:运行时恐慌,是一种只会在程序运行时才回抛出来的异常。通俗的来说,就是出现了一个异常。在panic被抛出之后,如果没有在程序里添加任何保护措施的话,程序就会在打印出panic的详情,终止运行。
recover: 可以让进入恐慌的流程中的 goroutine 恢复过来。通俗的来说,就是让出现异常的 goroutine 保持运行,可以防止因为一个协程的错误,导致整条主线程崩溃。
【案例】下面程序中主线程和协程test1负责打印,协程test2负责为数组赋值,但是很明显test2会出现越界问题。
func test1() {
for i := 0; i < 5; i++ {
fmt.Println("Hello", i)
time.Sleep(time.Second)
}
}
func test2() {
var arr [3]int
for i := 0; i < 5; i++ {
//数组容量为3,当i=3时越界
arr[i] = i
fmt.Printf("arr[%v]=%v\n", i, arr[i])
time.Sleep(time.Second)
}
}
func main() {
go test1()
go test2()
for i := 0; i < 10; i++ {
fmt.Println("main()", i)
time.Sleep(time.Second)
}
}
运行结果:
...
arr[2]=2
Hello 3
main() 3
panic: runtime error: index out of range [3] with length 3
可见,因为test2一个协程的panic,整个线程都崩溃了,这时就需要recover来处理这个问题。
func test1() {
for i := 0; i < 5; i++ {
fmt.Println("Hello", i)
time.Sleep(time.Second)
}
}
func test2() {
//defer + recover
defer func() {
//捕获test2出现的异常
if err := recover(); err != nil {
fmt.Println("test2() err!", err)
}
}()
var arr [3]int
for i := 0; i < 5; i++ {
arr[i] = i
fmt.Printf("arr[%v]=%v\n", i, arr[i])
time.Sleep(time.Second)
}
}
func main() {
go test1()
go test2()
for i := 0; i < 10; i++ {
fmt.Println("main()", i)
time.Sleep(time.Second)
}
}
运行结果:
...
arr[2]=2
Hello 3
main() 3
test2() err! runtime error: index out of range [3] with length 3
main() 4
Hello 4
...
test2打印信息后提前停止,test1和主线程正常运行直到结束。