参考:
https://draveness.me/golang/docs
goroutine and channel brief introduce
read the code below
package main
import (
"fmt"
"time"
)
// 求和,并将结果送入channel中
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
fmt.Println(sum)
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c) //17
go sum(s[len(s)/2:], c) //-5
fmt.Println("this is main goroutine")
// sleep 1 sec, 主goroutine休眠1秒钟
time.Sleep(time.Duration(1)*time.Second)
fmt.Println("this is still main goroutine")
x, y := <-c, <-c // receive from c
fmt.Println("result is", x, y, x+y)
}
以上代码run出2种结果
result1:
this is main goroutine // 主goroutine
17
-5
this is still main goroutine // 主goroutine
result is 17 -5 12
result2:
this is main goroutine // 主goroutine
-5
17
this is still main goroutine // 主goroutine
result is -5 17 12
- 可以看到两个执行相同代码的goroutine并非先开始就先结束,它们是concurrent并发的,顺序不可预测
- go sum的2个goroutine,谁先打印sum谁就先向chan写入数据。由于chan不带缓存,是一个同步chan,而从chan取数据的main goroutine sleep 1 sec,因此chan缺少receiver。go sum的2个goroutine在 c <- sum 位置产生阻塞
Buffered Channels
Channels can be buffered. Provide the buffer length as the second argument to make to initialize a buffered channel
ch := make(chan int, 100)
package main
import "fmt"
func main() {
ch := make(chan int, 2) // buffer为2的chan
fmt.Println("send 1")
ch <- 1
fmt.Println("send 2")
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
以上代码运行结果如下
send 1
send 2
1
2
当buffer未满时,向chan写数据永远不会阻塞,即使没有receiver;buffer满时,写入产生阻塞。通过缓存的使用,可以尽量避免阻塞,提供应用的性能
close channel
A sender can close a channel to indicate that no more values will be sent. Only the sender should close a channel, never the receiver. Closing is only necessary when the receiver must be told there are no more values coming, such as to terminate a range loop.
package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch<-1
ch<-2
close(ch)
for v := range ch {
fmt.Println(v)
}
v := <- ch
fmt.Println(v)
}
>>>
1
2
0
channel关闭之后,仍然可以从channel中读取剩余的数据,直到数据全部读取完成
而从对于一个关闭的channel,如果继续向channel发送数据,会引起panic;如果继续读数据,得到的是零值(对于int,就是0)
golang select - 监听多个channel
C语言中的 select 关键字可以同时【监听】多个文件描述符的可读或者可写的状态,Go语言中的 select 关键字也能够让 goroutine 同时【监听】多个 Channel 的可读或者可写,不可读或者不可写时,select 会一直阻塞当前的线程或者Goroutine
如果有同时多个channel可读/可写,那么Go会伪随机的选择一个channel并执行对应case后的操作(pseudo-random)
下面的代码展示了golang select的使用示例
package main
import (
"fmt"
)
func main() {
c := make(chan int, 10)
q := make(chan int, 10)
fibonacci(c, q)
}
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
ele := <-c
fmt.Println(ele, "to quit")
quit <- ele
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
上面的代码的输出有无数种可能
因为select结构会等待 c <- x 或者 <-quit 两个表达式中任意一个的返回。无论哪一个表达式返回都会立刻执行 case 中的代码,而当 select 中的两个 case 同时被触发时,会伪随机选择一个 case 执行
首次必然是case c <- x执行,其后则随机了。随机的引入避免了饥饿问题的发生
输出如
0 to quit
1 to quit
1 to quit
2 to quit
quit
0 to quit
quit
etc...
select更进一步
- select空case: 直接阻塞当前 Goroutine,导致 Goroutine 进入无法被唤醒的永久休眠状态
- select单case: 当 case 中的 Channel 是空指针时,会直接挂起当前 Goroutine 并陷入永久休眠,类似select空case
- select带有default的多case: 非阻塞式select
具体地:
在编译器对 select
语句进行优化之后,Go 语言会在运行时执行编译期间展开的 runtime.selectgo
函数,该函数会按照以下的流程执行:
- 随机生成一个遍历的轮询顺序
pollOrder
并根据 Channel 地址生成锁定顺序lockOrder
; - 根据
pollOrder
遍历所有的case
查看是否有可以立刻处理的 Channel;- 如果存在,直接获取
case
对应的索引并返回; - 如果不存在,创建
runtime.sudog
结构体,将当前 Goroutine 加入到所有相关 Channel 的收发队列,并调用runtime.gopark
挂起当前 Goroutine 等待调度器的唤醒;
- 如果存在,直接获取
- 当调度器唤醒当前 Goroutine 时,会再次按照
lockOrder
遍历所有的case
,从中查找需要被处理的runtime.sudog
对应的索引
使用goroutine避免踩坑
package main
import "fmt"
import "time"
func main() {
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(2 * time.Second)
}
//
10
10
10
10
10
10
10
10
10
10
for循环对同一变量i赋值,routine启动的时候才引用这个变量
闭包捕获变量都是语法层面的引用。如果变量通过调用goroutine时传入,就是拷贝,不会出现这个问题
package main
import "fmt"
import "time"
func main() {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
time.Sleep(2 * time.Second)
}
//
5
1
2
3
4
7
6
8
9
0
网友评论