1.goroutine
特点:
1.有独立的栈空间
2.共享程序堆空间
3.调度由用户控制
4.协程是轻量级线程
goroutine 是一个轻量级的执行单元,相比线程开销更小,完全由 Go 语言负责调度,是 Go 支持并发的核心。开启一个 goroutine 非常简单:
package main
import (
"fmt"
"time"
)
func main() {
go fmt.Println("goroutine message")
time.Sleep(1) //1
fmt.Println("main function message")
}
#1
的代码是必须的,这是为了让新开启的 goroutine 有机会得到执行,开启一个 goroutine 之后,后续的代码会继续执行,在上面的例子中后续代码执行完毕程序就终止了,而开启的 goroutine 可能还没开始执行。
如果尝试去掉 #1
处的代码,程序也可能会正常运行,这是因为恰巧开启的 goroutine 只是简单的执行了一次输出,如果 goroutine 中耗时稍长就会导致只能看到主一句 main function message
。
换句话话说,这里的 time.sleep
提供的是一种调度机制,这也是 Go 中 channel 存在的目的:负责消息传递和调度。
2.channel
“对于closed或nil通道,发送和接收操作都有相应规则:
1.向已关闭通道发送数据,引发panic。
2.从已关闭接收数据,返回已缓冲数据或零值。
3.无论收发,nil通道都会阻塞”
Channel 是 Go 中为 goroutine 提供的一种通信机制,借助于 channel 不同的 goroutine 之间可以相互通信。channel 是有类型的,而且有方向,可以把 channel 类比成 unix 中的 pipe。Go 通过 <-
操作符来实现 channel 的写和读,send value <-
在 channel 右侧,receive value <-
在左侧,receive value 不赋值给任何变量是合法的。
i := make(chan int)//int 类型
s := make(chan string)//字符串类型
r := make(<-chan bool)//只读
w := make(chan<- []int)//只写
<- w //合法的语句
Channel 最重要的作用就是传递消息。
package main
import (
"fmt"
)
func main() {
c := make(chan int)
go func() {
fmt.Println("goroutine message")
c <- 1 //1
}()
<-c //2
fmt.Println("main function message")
}
例子中声明了一个 int 类型的 channel,在 goroutine 中在代码 #1
处向 channel 发送了数据 1
,在 main 中 #2
处等待数据的接收,如果 c 中没有数据,代码的执行将发生阻塞,直到有 goroutine 开始往 c 中 send value。
这是 channel 最简单的用法之一:同步,这种类型的 channel 容量是 0,称之为 unbuffered channel。
3.Unbuffered channel
Channel 可以设置容量,表示 channel 允许接收的消息个数,默认的 channel 容量是 0 称为 unbuffered channel ,对 unbuffered channel 执行 读 操作 value := <-ch 会一直阻塞直到有数据可接收,执行 写 操作 ch <- value 也会一直阻塞直到有 goroutine 对 channel 开始执行接收,正因为如此在同一个 goroutine 中使用 unbuffered channel 会造成 deadlock。
package main
import (
"fmt"
)
func main() {
c := make(chan int)
c <- 1
<-c
fmt.Println("main function message")
}
执行报 fatal error: all goroutines are asleep - deadlock!
,读和写相互等待对方从而导致死锁发生。
4.Buffered channel
如果 channel 的容量不是 0,此类 channel 称之为 buffered channel ,buffered channel 在消息写入个数 未达到容量的上限之前不会阻塞 ,一旦写入消息个数超过上限,下次输入将会阻塞,直到 channel 有位置可以再写入。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OG7KyYuv-1584522286495)(http://static.git-star.com/Screen+Shot+2014-02-17+at+8.38.15+AM.jpg)]
package main
import (
"fmt"
)
func main() {
c := make(chan int, 3)
go func() {
for i := 0; i < 4; i++ {
c <- i
fmt.Println("write to c ", i)
}
}()
for i := 0; i < 4; i++ {
fmt.Println("reading", <-c)
}
}
输出:
write to c 0
reading 0
write to c 1
reading 1
write to c 2
reading 2
write to c 3
reading 3
根据上文对 buffered channel 的解释,这个例子中 channel c
的容量是 3,在写入消息个数不超过 3 时不会阻塞,输出应该是:
write to c 0
write to c 1
write to c 2
reading 0
reading 1
reading 2
write to c 3
reading 3
问题其实是在 fmt.Println
,一次输出就导致 goroutine 的执行发生了切换(相当于发生了 IO 阻塞),因而即使 c 没有发生阻塞 goroutine 也会让出执行,一起来验证一下这个问题。
package main
import (
"fmt"
"strconv"
)
func main() {
c := make(chan int, 3)
s := make([]string, 8)
var num int = 0
go func() {
for i := 0; i < 4; i++ {
c <- i
num++
v := "inner=>" + strconv.Itoa(num)
s = append(s, v)
}
}()
for i := 0; i < 4; i++ {
<-c
num++
v := "outer=>" + strconv.Itoa(num)
s = append(s, v)
}
fmt.Println(s)
}
这里创建了一个 slice 用来保存 c 进行写入和读取时的执行顺序,num 是用来标识执行顺序的,在没有加入 Println 之前,最终 s 是 [inner=>1 inner=>2 inner=>3 inner=>4 outer=>5 outer=>6 outer=>7 outer=>8] ,输出结果表明 c 达到容量上线之后才会发生阻塞。
相反有输出语句的版本结果则不同:
package main
import (
"fmt"
"strconv"
)
func main() {
c := make(chan int, 3)
s := make([]string, 8)
var num int = 0
go func() {
for i := 0; i < 4; i++ {
c <- i
num++
v := "inner=>" + strconv.Itoa(num)
s = append(s, v)
fmt.Println("write to c ", i)
}
}()
for i := 0; i < 4; i++ {
num++
v := "outer=>" + strconv.Itoa(num)
s = append(s, v)
fmt.Println("reading", <-c)
}
fmt.Println(s)
}
[outer=>1 inner=>2 outer=>3 inner=>4 inner=>5 inner=>6 outer=>7 outer=>8] 输出结果能表明两个 goroutine 是交替执行,也就是说 IO 的调用 Println 导致 goroutine 的让出了执行。
5.使用 select 读取多个 channel
Go 提供了 select 语句来处理多个 channel 的消息读取。
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
for {
c1 <- "from 1"
time.Sleep(time.Second * 2)
}
}()
go func() {
for {
c2 <- "from 2"
time.Sleep(time.Second * 2)
}
}()
go func() {
for {
select {
case msg1 := <-c1:
fmt.Println(msg1)
case msg2 := <-c2:
fmt.Println(msg2)
}
}
}()
var input string
fmt.Scanln(&input)
}
select 语句可以从多个可读的 channel 中随机选取一个执行,注意是 随机选取。
6.关闭 Channel
Channel 可以被关闭 close
,channel 关闭之后仍然可以读取,但是向被关闭的 channel send 会 panic。如果 channel 关闭之前有值写入,关闭之后将依次读取 channel 中的消息,读完完毕之后再次读取将会返回 channel 的类型的 zero value:
package main
import (
"fmt"
)
func main() {
c := make(chan int, 3)
go func() {
c <- 1
c <- 2
c <- 3
close(c)
}()
fmt.Println(<-c)
fmt.Println(<-c)
fmt.Println(<-c)
fmt.Println(<-c)
fmt.Println(<-c)
fmt.Println(<-c)
}
输出 1 2 3 0 0 0 ,0 是 int channel c 的 zero value。
package main
import (
"fmt"
)
func main() {
c := make(chan int, 3)
go func() {
c <- 1
c <- 2
c <- 3
close(c)
}()
for i := range c {
fmt.Println(i)
}
}
c 可以进行 range 迭代,如果 channel 没有被关闭 range 会一直等待 channel,但是关闭 channel 之后可以隐式的中断 range 的迭代
7.判断 channel 的关闭
Go 提供了 ok 表达式来判断 channel 的关闭状态。
value, ok <- c
如果 channel 是关闭状态,ok 是 false,value 是 channel 的 zero value,否则 ok 是 true 表示 channel 未关闭,value 表示 channel 中的值。
8.单向channel
默认情况下,通道是双向的,也就是,既可以往里面发送数据也可以从里面接收数据.
但是,我们经常见一个通道作为参数进行传递而只希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,
这时候我们可以指定通道的方向.
创建单向channel:
var ch1 chan int //这是一个正常的channel,不是单向的
var ch2 chan<- float64 //只用于写float64数据
var ch3 <-chan int //只用于读int数据
* chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出.
* <-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入
可以将channel隐式转换为单向队列,只收或只发,不能将单向channel转换为双向channel
单向channel特性:
ch := make(chan int) //创建一个双向channel
//双向channel隐式转换为单向channel
var WriteCh chan<- int = ch //只能写,不能读(输入端可以close)
var ReadCh <-chan int = ch //只能读,不能写(接收端不能close)
WriteCh <- 1000
<-ReadCh
接收端和输入端不能相互转换
单向channel的应用:
//此通道只能写,不能读
func producer(out chan<- int) {
for i := 0; i < 10; i++ {
fmt.Println("写",i)
out <- i * i
}
close(out)
}
//此通道只能读,不能写
func consumer(in <-chan int) {
for value := range in {
fmt.Println("读",value)
}
}
func main() {
ch := make(chan int)
go producer(ch) //生产者,生产数字,写入channel
consumer(ch) //消费者,从channel读取内容,打印
}
写 0
写 1
读 0
读 1
写 2
写 3
读 4
读 9
写 4
写 5
读 16
读 25
写 6
写 7
读 36
读 49
写 8
写 9
读 64
读 81