这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
GO进阶之协程开发
一、并发编程
1.1Goroutine概念
- 「线程」内核态,线程跑多个协程,栈 MB 级别
- 「协程」用户态,轻量级线程,栈 KB 级别
【使用】在函数前面加个go关键字,即可开启goroutine,但是要注意:主线程若执行结束,则这些协程都将关闭。
1.2 协程间通信
总共分为两种
- 通过通信共享内存
- 通过共享内存实现通信
在Go中,提倡通过通信共享内存而不是通过共享内存而实现通信,用平常话说就是尽量使用通道进行单向的传输,而不是双向。
channel定义
切记:channel 的声明必须使用 make 关键字,不能直接 var c chan int,这样得到的是 nil channel
channel 声明方法
-
双向通道:格式:
make(chan 元素类型, [缓冲大小])缓冲大小是可选参数,其代表着有无缓冲通道- 无缓冲通道(同步通道)----
make(chan int)
- 有缓冲通道---
make(chan, int, 2)
- 无缓冲通道(同步通道)----
-
[常用于函数]只读通道:只能从里读(
chan是箭头的起点),例如:func foo(ch1 <-chan int) // 只能从 ch1 里读 -
[常用于函数]只写通道:只能往里写(
chan是箭头的终点),例如:func bar(ch2 chan<- int) // 只能往 ch2 里写
channel 使用方法
这里要注意:变量v 需要和 ch 声明的元素类型相同
- 向
channel发送值:ch <- v
-
从
channel里读取结果:v <- ch- 注意通道的关闭函数
close(),会对 ch 发送一条消息,这个动作可以用来通知一些设定的goroutine退出。 - 并且读通道有个和
map特定的一个方法:v, f = <-ch //当关闭ch时,f返回 false -
package main func main() { done := make(chan struct{}) //这个done用于阻塞主线程,等待协程结束 c := make(chan int) //主线程生产传给协程 go func() { //这里调用close,会给done发消息,因此主线程的<-done就会解除阻塞 //主线程就能次于协程结束了! defer close(done) for { x, ok := <-c //close(c)时会收到一条消息,x值为0,ok为false if !ok { return //退出协程 } println(x) } }() c <- 1 c <- 2 c <- 3 close(c) <-done // close 时会收到消息,解除阻塞 }
- 注意通道的关闭函数
【如何循环取数】上面的代码里,我们用到了一个无限循环的方法,但正常使用,尝试用for-range的方法,可用下面的方法来代替:
//使用 for-range 遍历 channel 会比使用 ok-idiom(就是x:ok) 更简洁
//当c被close时,会退出for-range循环
for x := range c {
println(x)
}
/*
for {
x, ok := <-c //close(c)时会收到一条消息,x值为0,ok为false
if !ok {
return //退出协程
}
println(x)
}
*/
channel 关闭的基本法则
- 单
sender的情况下,都可以直接在sender端关闭channel。 - 多
sender的情况下,可以增加一个传递关闭信号的channel专门用于关闭数据传输的channel。
原则:不要从接收端关闭 channel,也不要在有多个发送端时,主动关闭 channel。
channel 对于有无缓冲通道的阻塞情况
select语句
select 是 Go 中的一个控制结构,类似于 switch 语句。
select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
【规则】select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行channel 的声明必须使用 make 关键字,不能直接 var c chan int,这样得到的是 nil channel相应的代码块。
-
如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。
否则:
- 如果有 default 子句,则执行该语句。
- 如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。
select {
case <- channel1:
// 执行的代码
case value := <- channel2:
// 执行的代码
case channel3 <- value:
// 执行的代码
// 你可以定义任意数量的 case
default:
// 所有通道都没有准备好,执行的代码
}
【用法】
-
阻塞main函数。有时候我们会让main函数阻塞不退出,如
http服务,我们会使用空的select{}来阻塞main,比空for更省性能 -
竞争选举。这个是最常见的使用场景,多个通道,有一个满足条件可以读取,就可以“竞选成功”
-
超时处理,如:
select { case str := <- ch1 fmt.Println("receive str", str) case <- time.After(time.Second * 5): fmt.Println("timeout!!") } -
判断buffered channel是否阻塞
这个例子很经典,比如我们有一个有限的资源(这里用buffer channel实现),我们每一秒向bufChan传送数据,由于生产者的生产速度大于消费者的消费速度,故会触发default语句,这个就很像我们web端来显示并发过高的提示了
package main
import (
"fmt"
"time"
)
func main() {
bufChan := make(chan int, 5)
go func () {
time.Sleep(time.Second)
for {
<-bufChan//消费者
time.Sleep(5*time.Second)
}
}()
for {
select {
case bufChan <- 1: //生产者
fmt.Println("add success")
time.Sleep(time.Second)
default:
fmt.Println("资源已满,请稍后再试")
time.Sleep(time.Second)
}
}
}
defer语句
defer是Go语言中的延迟执行语句,用来添加函数结束时执行的代码,常用于释放某些已分配的资源、关闭数据库连接、断开socket连接、解锁一个加锁的资源。Go语言机制担保一定会执行defer语句中的代码。
类似于C++的析构~
当有多个defer语句时,顺序是后入先出,如下例:
func main{
if true {
defer fmt.Printf("1")
} else {
defer fmt.Printf("2")
}
defer fmt.Printf("3")
//答案是3 1
}