Goroutine
- 协程最大优势在于"轻量级",可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万。
- 创建Goroutine时为其分配4k堆栈,随着程序的执行自动增长删除。创建线程时必须指定堆栈且是固定的,通常以M为单位。
- Thread创建和销毁都会有巨大的消耗,因为要和操作系统打交道,是内核级的,通常解决的办法就是线程池。而goroutine因为是由Go runtime负责管理的,创建和销毁的消耗非常小,是用户级。
- 线程切换时需要保存各种寄存器状态,以便恢复,goroutines切换只需要保存三个寄存器。线程切换约1000-1500纳秒,goroutine切换约200纳秒。
- go的协程是非抢占式的,由Go runtime主动交出控制权(对于开发者而言是抢占式的)。线程在时间片用完后,由CPU中断任务强行将其调度走,这时就必须多保存很多信息。所以Goroutine的切换比线程切换更容易
- 从进程到线程再到协程,其实是一个不断共享,不断减少切换成本的过程。
优雅地等子协程结束
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func Add(a int) int {
defer wg.Done()
fmt.Printf("num: %d\n", a)
return a
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go Add(i)
}
wg.Wait()
}
// 写法二
package main
import "sync"
func main() {
wg := sync.WaitGroup{}
wg.Add(10) // 加10
for i := 0; i < 10; i++ {
go func(a, b int) {
defer wg.Done() // 减1
}(i, i+1)
}
wg.Wait() // 等待
}
// 闭包情况一
func main() {
wg := sync.WaitGroup{}
wg.Add(10) // 加10
for i := 0; i < 10; i++ {
go func(a int) {
defer wg.Done() // 减1
// do something
fmt.Println(a)
}(i)
}
wg.Wait()
}
// $go run main.go
// 9
// 8
// 4
// 5
// 0
// 2
// 6
// 7
// 1
// 3
// 闭包情况二
func main() {
wg := sync.WaitGroup{}
wg.Add(10) // 加10
for i := 0; i < 10; i++ {
go func() {
defer wg.Done() // 减1
// do something
fmt.Println(i)
}()
}
wg.Wait()
}
// $go run main.go
// 10
// 2
// 10
// 10
// 10
// 10
// 10
// 10
// 3
// 10
捕获子协程的panic
-
何时会发生panic:
- 运行时错误会导致panic,比如数组越界、除0
- 程序主动调用panic(error)
-
panic会执行什么:
- 逆序执行当前goroutine的defer链
(recover从这里介入)
- 打印错误信息和调用堆栈
- 调用exit(2)结束整个进程
func Add(d int) { fmt.Println(d + 10) } func main() { a := 4 defer fmt.Println("1 defer") b := 2 defer fmt.Println("2 defer") c := a / b // panic(errors.New("my error")) defer fmt.Println("3 defer") Add(c) } // go run main.go // 12 // 3 defer // 2 defer // 1 defer
func Add(d int) { fmt.Println(d + 10) } func main() { a := 4 defer fmt.Println("1 defer") b := 2 defer fmt.Println("2 defer") c := a / b panic(errors.New("my error")) defer fmt.Println("3 defer") Add(c) } // go run main.go // 2 defer // 1 defer // panic: my error // goroutine 1 [running]: // main.main() // E:/go/main.go:31 +0x109 // exit status 2
- 逆序执行当前goroutine的defer链
defer
- defer在函数退出前被调用,注意不是在代码的return语句之前执行,因为return语句不是原子操作
- 如果发生panic,则之后注册的defer不会执行
- defer服从先进后出原则(栈),既一个函数里如果注册了多个defer,则按注册的逆序执行
recover恢复
不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存
chan struct{}
- channel仅作为协程间同步的工具,不需要传递具体的数据,管道类型可以用struct{}
- s := make(chan struct{])
- sc <- struct{}{}
空结构体变量的内存占用为0
,因此struct{}类型的管道比bool类型的管道还要省内存
关闭channel
- 只有当管道关闭时,才能通过
range遍历
管道里的数据,否则会发生fatal error - 管道关闭后读操作会立即返回,如果缓冲已空会返回"0值"
- e, ok := <-ch
- ok == true 代表管道还没有关闭
同步channel
- 创建同步管道 syncChann := make(chan int)
- 往管道里放数据 syncChann <- 1
- 从管道取出数据 v: <-syncChann 消费者
- 没有往管道里发生数据时,取操作会阻塞或fatal error(子协程)
- 取操作没有准备好时,往管道里发生数据会阻塞或fatal error: all goroutines are asleep - deadlock!注意不是panic,
通过recover不能获取
// 示例
package main
import (
"fmt"
"time"
)
var ch = make(chan int)
func consumer(ch chan int) {
for {
v := <-ch // 阻塞
fmt.Printf("取出:%d\n", v)
}
}
func main() {
go consumer(ch)
ch <- 5
time.Sleep(1 * time.Second)
}
// go run .\async_channel.go
// 取出:5
// 异常示例
package main
import (
"fmt"
"time"
)
var ch = make(chan int)
func consumer(ch chan int) {
for {
v := <-ch // 阻塞
fmt.Printf("取出:%d\n", v)
}
}
func main() {
defer func() { // recover无法获取异常
if err := recover(); err != nil {
fmt.Println(err)
}
}()
ch <- 5 // 先写管道
go consumer(ch) // 消费者协程
time.Sleep(1 * time.Second)
}
// go run .\async_channel.go
// fatal error: all goroutines are asleep - deadlock!
// goroutine 1 [chan send]:
// main.main()
// E:/go/async_channel.go:18 +0x40
// exit status 2
// 生产者协程,没有消费者阻塞住,不执行下去
package main
import (
"fmt"
"time"
)
var ch = make(chan int)
func consumer(ch chan int) {
for {
v := <-ch // 阻塞
fmt.Printf("取出:%d\n", v)
}
}
func main() {
go func() {
ch <- 5 // 阻塞住,不往下执行
fmt.Println("发生成功")
}()
time.Sleep(1 * time.Second)
}
// go run sync_channel.go // 不打印,因为阻塞住了
异步channel
- 创建异步管道 ch:=make(chan int, 8)
- 会创建一个环形缓冲队列,
队列满时
send操作会阻塞或fatal error //send在main主协程会fatal error,子协程里会阻塞
正常示例
package main
import "fmt"
var ch = make(chan int, 10)
func send(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
fmt.Printf("send %d\n", i)
}
}
func main() {
send(ch)
}
// go run .\chan_example.go
send 0
send 1
send 2
send 3
send 4
send 5
send 6
send 7
send 8
send 9
main主协程,send管道满了:fatal error
package main
import "fmt"
var ch = make(chan int, 10)
func send(ch chan int) {
for i := 0; i < 11; i++ {
ch <- i
fmt.Printf("send %d\n", i)
}
}
func main() {
send(ch)
}
// go run .\chan_example.go
send 0
send 1
send 2
send 3
send 4
send 5
send 6
send 7
send 8
send 9
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.send(0xc0000160b0)
E:/GO_project/chan_example.go:16 +0x54
main.main()
E:/GO_project/chan_example.go:22 +0x34
exit status 2
子协程管道满了会阻塞
package main
import "fmt"
var ch = make(chan int, 10)
func send(ch chan int) {
for i := 0; i < 11; i++ {
ch <- i
fmt.Printf("send %d\n", i)
}
}
func main() {
go send(ch)
time.Sleep(1 * time.Second)
}
// go run .\chan_example.go
send 0
send 1
send 2
send 3
send 4
send 5
send 6
send 7
send 8
send 9 // 观察到明显阻塞
main主协程,receive超出管道会fatal error
package main
import (
"fmt"
"time"
)
var ch = make(chan int, 10)
func receive(ch chan int) {
for i := 0; i < 5; i++ {
v := <-ch
fmt.Printf("receive %d\n", v)
}
}
func send(ch chan int) {
for i := 0; i < 1; i++ {
ch <- i
fmt.Printf("send %d\n", i)
}
}
func main() {
send(ch)
receive(ch)
time.Sleep(1 * time.Second)
}
// go run .\chan_example.go
send 0
receive 0
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.receive(0xc00009c000)
E:/GO_project/chan_example.go:12 +0x58
main.main()
E:/GO_project/chan_example.go:26 +0x44
exit status 2
子协程管道取不出值,会阻塞
package main
import (
"fmt"
"time"
)
var ch = make(chan int, 10)
func receive(ch chan int) {
for i := 0; i < 5; i++ {
v := <-ch
fmt.Printf("receive %d\n", v)
}
}
func send(ch chan int) {
for i := 0; i < 1; i++ {
ch <- i
fmt.Printf("send %d\n", i)
}
}
func main() {
send(ch)
go receive(ch)
time.Sleep(1 * time.Second)
}
// go run .\chan_example.go
send 0
receive 0
使用缓冲示例
package main
import (
"fmt"
"time"
)
var ch = make(chan int, 10)
func receive(ch chan int) {
for i := 0; i < 5; i++ {
v := <-ch
fmt.Printf("receive %d\n", v)
}
}
func send(ch chan int) {
for i := 0; i < 15; i++ { // 超出了缓冲管道范围
ch <- i
fmt.Printf("send %d\n", i)
}
}
func main() {
go send(ch)
go receive(ch)
time.Sleep(1 * time.Second)
}
// go run .\chan_example.go
send 0
send 1
send 2
send 3
send 4
send 5
send 6
send 7
send 8
send 9
send 10
receive 0
receive 1
receive 2
receive 3
receive 4
send 11
send 12
send 13
send 14
管道先取值的话
func main() {
go receive(ch)
go send(ch)
time.Sleep(1 * time.Second)
}
// go run .\chan_example.go
send 0
send 1
send 2
send 3
send 4
send 5
send 6
receive 0
receive 1
receive 2
receive 3
receive 4
send 7
send 8
send 9
send 10
send 11
send 12
send 13
send 14
子协程套子协程(孙协程)
- 子协程销毁,主协程还在运行,“孙”协程依旧可以运行至结束
package main
import (
"fmt"
"time"
)
func grandson() {
fmt.Println("grnadson channel")
time.Sleep(3 * time.Second)
fmt.Println("grandson end")
}
func son() {
fmt.Println("son channel")
go grandson()
time.Sleep(1 * time.Second)
fmt.Println("son end")
}
func main(){
fmt.Println("main channel")
go son()
time.Sleep(4 * time.Second)
fmt.Println("main end")
}
// go run grandson_channel.go
main channel
son channel
grnadson channel
son end
grandson end
main end
- 主协程销毁,所有子/孙协程都会提前退出
package main
import (
"fmt"
"time"
)
func grandson() {
fmt.Println("grnadson channel")
time.Sleep(3 * time.Second)
fmt.Println("grandson end")
}
func son() {
fmt.Println("son channel")
go grandson()
time.Sleep(1 * time.Second)
fmt.Println("son end")
}
func main(){
fmt.Println("main channel")
go son()
time.Sleep(2 * time.Second)
fmt.Println("main end")
}
// go run .\grandson_channel.go
main channel
son channel
grnadson channel
son end
main end
- 主、子、孙运行时间相同,概率性同时结束或子孙提前退出
package main
import (
"fmt"
"time"
)
func grandson() {
fmt.Println("grnadson channel")
time.Sleep(1 * time.Second)
fmt.Println("grandson end")
}
func son() {
fmt.Println("son channel")
go grandson()
time.Sleep(1 * time.Second)
fmt.Println("son end")
}
func main(){
fmt.Println("main channel")
go son()
time.Sleep(1 * time.Second)
fmt.Println("main end")
}
GMP模型
- G(Goroutine)本质是一种轻量级的线程。蓝色是正在执行的Goroutine,灰色在等待队列中。
- M(Machine)对应一个内核线程
- P(Processor)虚拟处理器,代表M所需的上下文环境,是处理用户级代码逻辑的处理器。P的数量由环境变量中的GOMAXPROCS决定,默认情况下就是核数。
- GMP详情
- M和内核线程的对应关系是确定的
- M进入系统调用时,会抛弃P,P被挂在其它M上,然后继续执行G队列
- 系统调用返回后,相应的G进入全局的可运行队列(runqueue)中,P会周期性扫描这个全局的runqueue,使上面的G得到执行