这是我参与「第三届青训营 -后端场」笔记创作活动的第1篇笔记
1,Go 程(goroutine)是由 Go 运行时管理的轻量级线程,
1
go f(x, y, z) // 会启动一个新的 Go 程并执行 f(x, y, z),
2,在单一Go程中,事件发生的顺序即为程序所表达的顺序。
3,若以下条件均成立,则对变量 v 的读取操作 r 就允许对 v 的写入操作 w 进行监测(r读取w写入内容):r不发生在w之前;在 w 之后 r 之前,不存在其它对 v 进行的写入操作w。
4,当多个Go程访问共享变量 v 时,它们必须通过同步事件而不是程序顺序来建立发生顺序的条件,以此确保读取操作能监测到预期的写入。
1
var a string
2
3
func f() {
4
print(a)
5
}
6
7
func hello() {
8
a = "hello, world"
9
go f() // go 语句会在当前Go程开始执行前启动创建的新的的Go程。
10
}
11
// 输出空字符串
12
fmt.Println(a)
5,Go程无法确保在程序中的任何事件发生之前退出。若一个Go程的作用必须被另一个Go程监测到,需使用锁或信道通信之类的同步机制来建立顺序关系。读取操作 r 可能监测到与其并发的写入操作 w 写入的值。并不意味着发生在 r 之后的读取操作会监测到发生在 w 之前的写入操作。要使用显式的同步解决这些问题。
1
var a, b int
2
func f() {
3
a = 1 //a,b多步操作,非原子性
4
b = 2
5
}
6
func g() {
7
print(b)
8
print(a)
9
}
10
// 2 0,在其他线程中能检测到b发生变化,未必能检测到a也发生了变化。
11
func main() {
12
go f()
13
g()
14
}
1
type T struct {
2
msg string
3
}
4
5
var g *T
6
7
func setup() {
8
t := new(T)
9
t.msg = "hello, world"
10
g = t
11
}
12
// 即便 main 能够监测到 g != nil 并退出循环, 它也无法保证能监测到 g.msg 的初始化值。 线程缓存未及时写入主存。
13
func main() {
14
go setup()
15
for g == nil {
16
}
17
print(g.msg)
18
}
信道
1,Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。信道是带有类型的管道,可以通过信道操作符 <-、-> 来发送或者接收值。 默认情况下,发送和接收操作在另一端准备好之前都会阻塞。
1
ch := make(chan int) //创建信道
2
ch <- v // 将 v 发送至信道 ch。
3
v := <-ch // 从 ch 接收值并赋予 v。
2,通道的的基本特性:
- 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。在同一时刻,Go 语言的运行时系统只会执行对同一个通道的任意个发送操作中的某一个。直到这个元素值被完全复制进该通道之后,其他针对该通道的发送操作才可能被执行。在同一时刻,运行时系统也只会执行对同一个通道的任意个接收操作中的某一个。直到这个元素值完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。
- 发送操作和接收操作中对元素值的处理都是不可分割的。对于通道中的同一个元素值来说,发送操作和接收操作之间也是互斥的。元素值从外界进入通道时会被复制。进入通道的并不是在接收操作符右边的那个元素值,而是它的副本(浅拷贝,针对缓冲通道)。元素值从通道进入外界时会被复制(浅拷贝,针对缓冲通道),第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。缓冲通道会作为收发双方的中间件。元素值会先从发送方复制到缓冲通道,之后再由缓冲通道复制给接收方(浅拷贝,针对缓冲通道)。当发送操作在执行的时候发现空的通道中,正好有等待的接收方,那么它会直接把元素值复制给接收方。复制方式为浅拷贝:只是拷贝值以及值中直接包含的东西, 对于复合结构只拷贝他的直接成员。处理元素值时都是一气呵成的,绝不会被打断(原子性)。发送操作要么还没复制元素值,要么已经复制完毕,绝不会出现只复制了一部分的情况。接收操作在准备好元素值的副本之后,一定会删除掉通道中的原值,绝不会出现通道中仍有残留的情况。无缓冲信道不存在缓冲拷贝,发送发直接阻塞,当接收方到来时直接复制给接收方。
- 发送操作在完全完成之前会被阻塞。接收操作也是如此。发送操作包括了“复制元素值”和“放置副本到通道内部”这两个步骤。在这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里。接收操作通常包含了“复制通道内的元素值”“放置副本到接收方”“删掉原值”三个步骤。在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞。阻塞代码其实就是为了实现操作的互斥和元素值的完整。当多个发送操作被阻塞,现信道可用时:通道会优先通知最早因此而等待的、那个发送操作所在的 goroutine,后者会再次执行发送操作。如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。通道会通知最早等待的那个接收操作所在的 goroutine,并使它再次执行接收操作。对于值为
nil的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态。
3,信道是双向的,但只能接受到对方法发来的消息(半双工)使用信道Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
1
func sum(s []int, c chan int) {
2
sum := 0
3
for _, v := range s {
4
sum += v
5
}
6
c <- sum // 将和送入 c
7
}
8
9
func main() {
10
s := []int{7, 2, 8, -9, 4, 0}
11
c := make(chan int)
12
go sum(s[:len(s)/2], c)
13
go sum(s[len(s)/2:], c)
14
x, y := <-c, <-c // 从 c 中接收,在两个go程都运行完前当前go程在阻塞
15
fmt.Println(x, y, x+y)
16
}
17
4,单向通道:只能发送数据:make(chan <- int, 1);只能接受数据: make(<- chan int, 1),通常声明一个只有一端(发送端或者接收端)能用的通道没有任何意义,单向通道最主要的用途就是约束其他代码的行为。调用时:只需要把一个元素类型匹配的双向通道传给它就行了, Go 语言在这种情况下会自动地把双向通道转换为函数所需的单向通道。在方法内部只能使用函数声明的单向信道的功能。
1
type Notifier interface {
2
SendInt(ch chan<- int) //在该接口的所有实现类型中的SendInt方法都会受到限制。
3
}
5,它将共享的值通过信道传递,实际上,多个独立执行的线程从不会主动共享。在任意给定的时间点,只有一个Go程能够访问该值。不要通过共享内存来通信,而应通过通信来共享内存。
6,无缓冲信道(容量为0)在通信时会同步交换数据,它能确保(两个Go程的)计算处于确定状态。无缓冲信道不能存储数据,往里面写数据后写go程会被阻塞,直到里面的数据被取走。无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。非缓冲通道是在用同步的方式传递数据。只有收发双方对接上了,数据才会被传递。数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。相比之下,缓冲通道则在用异步的方式传递数据。信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:
1
ch := make(chan int, 100)
仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。 若信道是不带缓冲的,那么在接收者收到值前, 发送者会一直阻塞;若信道是带缓冲的,则发送者仅在值被复制到缓冲区前阻塞;若缓冲区已满,发送者会一直等待直到某个接收者取出一个值空出一个缓冲区为止。
7,通道的长度(len())代表通道当前包含的元素个数,容量(cap())就是初始化时你设置的那个数。
8,由于数据同步发生在信道的接收端(发送发生在接受之前),信号必须在信道的接收端获取,而非发送端。
9,带缓冲的信道可被用作信号量,例如限制吞吐量。
方案一:尽管只有 MaxOutstanding 个Go程能同时运行,但 Serve 还是为每个进入的请求都创建了新的Go程,只是只有MaxOutstabding个操作同时进行,其它操作还是被创建出来,只是Go程被阻塞。若请求来得很快, 该程序就会无限地消耗资源。
1
2
var sem = make(chan int, MaxOutstanding)
3
4
func handle(r *Request) {
5
sem <- 1 // 往信道中写数据,标志占用一个资源
6
process(r) // 可能需要很长时间。
7
<-sem // 往信道中取数据,标志释放一个资源
8
}
9
10
func Serve(queue chan *Request) {
11
for {
12
req := <-queue
13
go handle(req) // 无需等待 handle 结束。
14
}
15
}
方案二 :循环变量req在每次迭代时会被重用,因此 req 变量会被在所有的Go程间共享
1
func Serve(queue chan *Request) {
2
for req := range queue {
3
sem <- 1
4
go func() {
5
process(req)
6
<-sem
7
}()
8
}
我们需要确保 req 对于每个Go程来说都是唯一的。可以将 req 的值作为实参传入到该Go程的闭包中来实现:
1
func Serve(queue chan *Request) {
2
for req := range queue {
3
sem <- 1
4
go func(req *Request) { //将当前req与函数绑定,立即执行
5
process(req)
6
<-sem
7
}(req)
8
}
9
}
方案三:以相同的名字创建新的变量,req := req在Go中这样做是合法且惯用的。用相同的名字获得了该变量的一个新的副本, 以此来局部地刻意屏蔽循环变量,使它对每个Go程保持唯一。
1
func Serve(queue chan *Request) {
2
for req := range queue {
3
req := req // 为该Go程创建 req 的新实例。
4
sem <- 1
5
go func() {
6
process(req)
7
<-sem
8
}()
9
}
10
}
方案四:启动固定数量的 handle Go程,一起从请求信道中读取数据。Go程的数量限制了同时调用 process 的数量。Serve 同样会接收一个通知退出的信道, 在启动所有Go程后,它将阻塞并暂停从信道中接收消息。
1
func handle(queue chan *Request) {
2
for r := range queue { // 从quene中取出还没被处理的请求,quene长度减一
3
process(r)
4
}
5
}
6
7
func Serve(clientRequests chan *Request, quit chan bool) {
8
// 启动处理程序
9
for i := 0; i < MaxOutstanding; i++ {
10
go handle(clientRequests) //MaxOutstanding个handle同时从clientRequests获取任务处理请求
11
}
12
<-quit // 等待通知退出。
13
}
10,信道是数值,它可以被分配并像其它值到处传递。 这种特性通常被用来实现安全、并行的多路分解。若该类型包含一个可用于回复的信道, 那么每一个客户端都能为其回应。以下为 Request 类型的大概定义。
1
var sem = make(chan int, MaxOutstanding)
2
var clientRequests=make(chan *Request,10)
3
// 客户端提供了一个函数及其实参,此外在请求对象中还有个接收应答的信道。
4
type Request struct {
5
args []int
6
f func([]int) int
7
resultChan chan int
8
}
9
func sum(a []int) (s int) {
10
for _, v := range a {
11
s += v
12
}
13
return
14
}
15
16
func handle(req *Request) {
17
req.resultChan <- req.f(req.args)
18
}
19
20
func Serve(queue chan *Request) {
21
for req := range queue {
22
sem <- 1
23
go func(req *Request) { //闭包将当前req与函数绑定
24
handle(req)
25
<-sem
26
}(req)
27
}
28
}
29
30
31
32
func main(){
33
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
34
// 发送请求
35
clientRequests <- request
36
Serve(clientRequests)
37
// 等待回应
38
fmt.Printf("answer: %d\n", <-request.resultChan)
39
}
11,发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:v, ok := <-ch如果通道关闭时,里面还有元素值未被取出,v仍会是通道中的某一个元素值,而ok一定会是true。通过ok,来判断通道是否关闭是可能有延时的。若没有值可以接收且信道已被关闭:ok =false,不要让接收方关闭通道,而应当让发送方做这件事。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。 试图关闭一个已经关闭了的通道,也会引发 panic,若在信道关闭后从中接收数据,并且信道内为空,接收者就会收到该信道返回的零值。信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭
1
func fibonacci(n int, c chan int) {
2
x, y := 0, 1
3
for i := 0; i < n; i++ {
4
c <- x
5
x, y = y, x+y
6
}
7
close(c) //终止一个 range 循环,不然range不会终止。
8
}
9
func main() {
10
c := make(chan int, 5)
11
go fibonacci(10, c)
12
for i := range c { //依据len调用,在取出值和加入值后len都会变化
13
fmt.Println(i)
14
}
15
}
12,循环 for i := range c 会不断从信道接收值,等价于i<-c,当无数据可以读取时就会堵塞,直到它被关闭。
13, select 语句使一个 Go 程可以等待多个通信操作。 :每个case表达式中都必须包含通道的读或者写;select语句会查看哪些case的读写操作能成功执行,只是查看能否执行,不是真的执行,然后开始选择能成功执行的候选分支,进行读写操作,执行对应case内容,然后结束当前select ;当多个分支都准备好时会随机选择一个执行case,而随机的引入就是为了避免饥饿问题的发生,然后结束当前select 。如果所有的候选分支都不满足选择条件,那么默认分支就会被执行,如果这时没有默认分支,那么select语句就会立即进入阻塞状态,直到至少有一个候选分支满足选择条件为止。一旦有一个候选分支满足选择条件,select语句就会被唤醒,这个候选分支就会被执行。
为了在尝试发送或者接收时不发生阻塞,可使用 default 分支。如果在select语句中发现某个通道已关闭,可以把该信道设为 nil 屏蔽掉它所在的分支,简单地在select语句的分支中使用break语句,只能结束当前的select语句的执行,在select语句与for语句联用时可以设置标志位与goto实现跳出循环。
1
//break 方式
2
oop:
3
for {
4
select {
5
case _, ok := <-ch1: //ch1非空或许信道被关闭且没有值时执行此语句
6
if !ok {
7
ch1 = nil //ch1已经关闭且没有值,将他设置为nil,以屏蔽ch1
8
}
9
fmt.Println("ch1")
10
case _, ok := <-ch2: //ch2非空或许信道被关闭且没有值时执行此语句
11
if !ok {
12
break loop //跳出for循环
13
}
14
fmt.Println("ch2")
15
default: // 所有分支都阻塞时执行此分支
16
time.Sleep(50 * time.Millisecond)
17
}
18
}
19
fmt.Println("END")
20
// goto方式
21
for {
22
select {
23
case _, ok := <-ch1: //ch1非空或许信道被关闭且没有值时执行此语句
24
if !ok {
25
ch1 = nil //ch1已经关闭且没有值,将他设置为nil,以屏蔽ch1
26
}
27
fmt.Println("ch1")
28
case _, ok := <-ch2: //ch2非空或许信道被关闭且没有值时执行此语句
29
if !ok {
30
goto loop //跳出for循环
31
}
32
fmt.Println("ch2")
33
default: // 所有分支都阻塞时执行此分支
34
time.Sleep(50 * time.Millisecond)
35
}
36
}
37
loop:
38
fmt.Println("END")