敞开生长之旅!这是我参与「日新方案 2 月更文挑战」的第 1 天,点击查看活动详情
耐性和耐久胜过激烈和疯狂。
哈喽大家好,我是陈明勇,本文介绍的内容是 Go
并发模块的两个重要人物 → goroutine
与 channel
。假如本文对你有协助,不妨点个赞,假如你是 Go
言语初学者,不妨点个关注,一起生长一起进步,假如本文有错误的当地,欢迎指出!
前语
Go 言语的 CSP
并发模型的完成包含两个主要组成部分:一个是 Goroutine
,另一个是 channel
。本文将会介绍它们的根本用法和留意事项。
Goroutine
Goroutine
是 Go
使用的根本履行单元,它是一种轻量的用户级线程,其底层是经过 coroutine
(协程)去完成的并发。众所周知,协程是一种运转在用户态的用户线程,因而 Goroutine
也是被调度于 Go
程序运转时。
根本用法
语法:go + 函数/办法
经过 go 关键字 + 函数/办法 能够创立一个 Goroutine
。
代码示例:
import (
"fmt"
"time"
)
func printGo() {
fmt.Println("签字函数")
}
type G struct {
}
func (g G) g() {
fmt.Println("办法")
}
func main() {
// 根据签字函数创立 goroutine
go printGo()
// 根据办法创立 goroutine
g := G{}
go g.g()
// 根据匿名函数创立 goroutine
go func() {
fmt.Println("匿名函数")
}()
// 根据闭包创立 goroutine
i := 0
go func() {
i++
fmt.Println("闭包")
}()
time.Sleep(time.Second) // 避免 main goroutine 完毕后,其创立的 goroutine 来不及运转,因而在此休眠 1 秒
}
履行结果:
闭包
签字函数
办法
匿名函数
当多个 Goroutine
存在时,它们的履行顺序是不固定的。因而每次打印的结果都不相同。
由代码可知,经过 go
关键字,咱们能够根据 签字函数 / 办法 创立 goroutine
,也能够根据 匿名函数 / 闭包 创立 goroutine
。
那么 Goroutine
是怎么退出的呢?正常状况下,只要 Goroutine
函数履行完毕,或许履行返回,意味着 Goroutine
的退出。假如 Goroutine
的函数或办法有返回值,在 Goroutine
退出时会将其忽略。
channel
channel
在 Go 并发模型中扮演者重要的人物。它能够用于完成 Goroutine
间的通讯,也能够用来完成 Goroutine
间的同步。
channel 的根本操作
channel
是一种复合数据类型,声明时需求指定 channel
里元素的类型。
声明语法:var ch chan string
经过上述代码声明一个元素类型为 string
的 channel
,其只能存放 string
类型的元素。channel
是引用类型,必须初始化才能写入数据,经过 make
的方式初始化。
import (
"fmt"
)
func main() {
var ch chan string
ch = make(chan string, 1)
// 打印 chan 的地址
fmt.Println(ch)
// 向 ch 发送 "Go" 数据
ch <- "Go"
// 从 ch 中接纳数据
s := <-ch
fmt.Println(s) // Go
}
经过 ch <- xxx
能够向 channel
变量 ch
发送数据,经过 x := <- ch
能够从 channel
变量 ch
中接纳数据。
带缓冲 channel 与无缓冲 channel
假如初始化 channel
时,不指定容量时,则创立的是一个无缓冲的 channel
:
ch := make(chan string)
无缓冲的 channel
的发送与接纳操作是同步的,在履行发送操作之后,对应 Goroutine
将会阻塞,直到有另一个 Goroutine
去履行接纳操作,反之亦然。假如将发送操作和履行操作放在同一个 Goroutine 下进行,会产生什么操作呢?看看下述代码:
import (
"fmt"
)
func main() {
ch := make(chan int)
// 发送数据
ch <- 1 // fatal error: all goroutines are asleep - deadlock!
// 接纳数据
n := <-ch
fmt.Println(n)
}
程序运转之后,会在 ch <-
处得到 fatal error
,提示一切的 Goroutine
处于休眠状态,也就是死锁了。为避免这种状况,咱们需求将 channel
的发送操作和接纳操作放到不同的 Goroutine
中履行。
import (
"fmt"
)
func main() {
ch := make(chan int)
go func() {
// 发送数据
ch <- 1
}()
// 接纳数据
n := <-ch
fmt.Println(n) // 1
}
由上述例子能够得出结论:无缓冲 channel
的发送与接纳操作,一定要放在两个不同的 Goroutine
中进行,不然会产生 deadlock
形象。
假如指定容量,则创立的是一个带缓冲的 channel
:
ch := make(chan string, 5)
有缓冲的 channel
与无缓冲的 chennel
有所区别,履行发送操作时,只要 channel
的缓冲区未满,Goroutine
不会挂起,直到缓冲区满时,再向 channel
履行发送操作,才会导致 Goroutine
挂起。代码示例:
func main() {
ch := make(chan int, 1)
// 发送数据
ch <- 1
ch <- 2 // fatal error: all goroutines are asleep - deadlock!
}
声明 channel 的只发送类型和只接纳类型
-
既能发送又能接纳的
channel
ch := make(chan int, 1)
经过上述代码取得
channel
变量,咱们能够对它履行发送与接纳的操作。 -
只接纳的
channel
ch := make(<-chan int, 1)
经过上述代码取得
channel
变量,咱们只能对它进行接纳操作。 -
只发送的
channel
ch := make(chan<- int, 1)
经过上述代码取得
channel
变量,咱们只能对它进行发送操作。
一般只发送 channel
类型和只接纳 channel
类型,会被用作函数的参数类型或返回值:
func send(ch chan<- int) {
ch <- 1
}
func recv(ch <-chan int) {
<-ch
}
channel 的封闭
经过内置函 close(c chan<- Type)
,能够对 channel
进行封闭。
- 在发送端封闭
channel
在channel
封闭之后,将不能对channel
履行发送操作,不然会产生panic
,提示channel
已封闭。func main() { ch := make(chan int, 5) ch <- 1 close(ch) ch <- 2 // panic: send on closed channel }
- 管道
channel
之后,依旧能够对channel
履行接纳操作,假如存在缓冲区的状况下,将会读取缓冲区的数据,假如缓冲区为空,则获取到的值为channel
对应类型的零值。import "fmt" func main() { ch := make(chan int, 5) ch <- 1 close(ch) fmt.Println(<-ch) // 1 n, ok := <-ch fmt.Println(n) // 0 fmt.Println(ok) // false }
- 假如经过 for-range 遍历
channel
时,中途封闭channel
则会导致for-range
循环完毕。
小结
本文首先介绍了 Goroutine
的创立方式以及其退出的机遇是什么。
其次介绍了怎么创立 channel
类型变量的有缓冲与无缓冲的创立方式。需求留意的是,无缓冲的 channel
发送与接纳操作,需求在两个不同的 Goroutine
中履行,不然会发送 error
。
接下来介绍怎么定义只发送和只接纳的 channel
类型。一般只发送 channel
类型和只接纳 channel
类型,会被用作函数的参数类型或返回值。
最后介绍了怎么封闭 channel
,以及封闭之后的一些留意事项。