Go channel [golang学习笔记5]
1.Go 对 CSP 的实现
要想理解 goalng channel 要先知道 CSP 模型。CSP 是 Communicating Sequential Process 的简称,中文可以叫做通信顺序进程,是一种并发编程模型。
Go 实现了两种并发形式。第一种是大家普遍认知的:多线程共享内存。其实就是 Java 或者 C++ 等语言中的多线程开发。
另外一种是Go语言特有的,也是Go语言推荐的:CSP 并发模型,不同于传统的多线程通过共享内存来通信,CSP 讲究的是“以通信的方式来共享内存”。Go 的 CSP 并发模型,是通过 goroutine 和 channel 来实现的。
goroutine
在go里面,每一个并发执行的活动成为goroutine。通过go语句进行创建。
goroutine(协程) 是Go语言中并发的执行单位。goroutine 可以认为是轻量级的线程,与创建线程相比,创建成本和开销都很小,每个goroutine 的堆栈只有几kb,并且堆栈可根据程序的需要增长和缩小(线程的堆栈需指明和固定),所以go程序从语言层面支持了高并发。
channel
channel 是 Go 语言中各个并发结构体( goroutine )之前的通信机制。 通俗的讲,就是各个 goroutine 之间通信的 “管道”。
CSP 模型的关键是关注 channel,而不关注发送消息的实体。Go 语言实现了 CSP 部分理论,goroutine 对应 CSP 中并发执行的实体,channel 也就对应着 CSP 中的 channel。
2. goroutine的实现
在函数或者方法前面加上关键字go,即创建一个并发运行的新goroutine。
func main() {
go func() {
fmt.Println("Hello world!")
}()
time.Sleep(2 * time.Second)
}
需要注意的是,执行速度很快,一定要加 sleep,避免 goroutine 未执行, main 方法就结束了,这样就看不到goroutine里的 “Hello world!” 输出。
这也说明了一个关键点:当main函数返回时,所有的gourutine都是暴力终结的,然后程序退出。
3. channel的实现
关于关闭 channel 有几点需要注意的是:
重复关闭 channel 会导致 panic。
向关闭的 channel 发送数据会 panic。
从关闭的 channel 读数据不会 panic,读出 channel 中已有的数据之后再读就是 channel 类似的默认值,比如 chan int 类型的 channel 关闭之后读取到的值为 0。
go channel 有两种,一种是带缓冲区,一种是不带缓冲区的。
无缓冲:发送和接收动作是同时发生的。如果没有 goroutine 读取 channel (<- channel),则发送者 (channel <-) 会一直阻塞。
缓冲:缓冲 channel 类似一个有容量的队列。当队列满的时候发送者会阻塞;当队列空的时候接收者会阻塞。直接上例子。
不带缓冲区:
func test1() {
fmt.Println("make chan")
done := make(chan bool) //不设置缓冲
fmt.Println("goroutine")
go func() {
fmt.Println("Hello world goroutine")
time.Sleep(1 * time.Second)
fmt.Println("done <- true")
done <- true
}()
fmt.Println("<-done") //被阻塞
fmt.Println("return")
}
无缓冲通道上的发送操作将会被阻塞,直到另一个goroutine在对应的通道上执行接收操作,此时值才传送完成,程序继续执行。
带缓冲区:
func main() {
messages := make(chan string, 2) //设置缓冲大小
go func() {
fmt.Println("ping")
messages <- "ping"
fmt.Println("pong")
messages <- "pong"
fmt.Println("ping pong")
messages <- "ping pong" // goroutine会在这里阻塞
}()
fmt.Println("1", <-messages)
fmt.Println("2", <-messages)
fmt.Println("3", <-messages)
}
goroutine 向 channel 发送数据的时候如果缓冲还没满,那么该goroutine 就不会阻塞。反之,如果接受该 channel 数据的时候,如果缓冲有数据,那么该 goroutine 就不会阻塞。
管道,串联的channel(Pipeline)
channel 也可以用于将多个 goroutine 链接在一起,一个 channel 的输出作为下一个 channel 的输入。这种串联的 channel 就是所谓的管道(pipeline)。
func main() {
echo := make(chan string)
receive := make(chan string)
go func() {
time.Sleep(1 * time.Second)
echo <- "echo test"
}()
go func() {
temp := <-echo
receive <- temp
}()
fmt.Println(<-receive)
//在这里不一定要去关闭channel,因为底层的垃圾回收机制会根据它是否可以访问来决定是否自动回收它。
//(这里不是根据channel是否关闭来决定的)
}
单向通道类型
当程序则够复杂的时候,为了代码可读性更高,拆分成一个一个的小函数是需要的此时go提供了单向通道的类型,来实现函数之间channel的传递。
使用 channel 来使不同的goroutine去进行通信,很多时候都和消费者生产者模式很相似,一个 goroutine 生产的结果都用 channel 传送给另一个 goroutine,一个 goroutine 的执行依赖与另一个 goroutine 的结果。
因此很多情况下,channel 都是单方向的,在 go 里面可以把一个无方向的 channel 转换为只接受或者只发送的 channel,但是却不能反过来把接受或发送的 channel 转换为无方向的 channel,适当地把 channel 改成单方向,可以达到程序强约束的做法。
单向通道和双向通过的区别:
类型 chan<- string 表示一个只发送 string 的 channel,只能发送不能接收
相反,类型 <-chan int 表示一个只接收 string 的channel,只能接收不能发送
(箭头<-和关键字chan的相对位置表明了channel的方向。)这种限制将在编译期检测
func main() {
echo := make(chan string)
receive := make(chan string)
go func(out chan<- string) {
time.Sleep(1 * time.Second)
echo <- "echo test"
close(out)
}(echo)
//out chan<- string 单向通道,只用于写(发送) string 类型数据
//in <-chan string 单向通道,只用于读取(接收) string 类型数据
go func(out chan<- string, in <-chan string) {
temp := <-in //阻塞等待echo的通道的返回
out <- temp
close(out)
}(receive, echo)
fmt.Println(<-receive)
}
注意:常见的几个goroutine死锁
①如果使用 channel 之前没有 make,会出现 dead lock 错误,如下:
func main() {
var str chan string
go func() {
str <- "Try a test."
}()
fmt.Println(<-str)
}
错误消息:
fatal error: all goroutines are asleep - deadlock!
②没有创建 goroutine, 直接使用 无缓冲channel ,会出现 dead lock 错误
func main() {
echo := make(chan string)
echo <- "echo test"
fmt.Println(<-echo)
}
③没有创建 goroutine, 直接使用 缓冲channel ,超出缓冲时被阻塞,会出现 dead lock 错误
func main() {
echo := make(chan string, 1)
echo <- "echo test1"
echo <- "echo test2" //被阻塞
fmt.Println(<-echo)
}
select多路复用
在一个goroutine里面,对channel的操作很可能导致我们当前的goroutine阻塞,而我们之后的操作都进行不了。而如果我们又需要在当前channel阻塞进行其他操作,如操作其他channel或直接跳过阻塞,可以通过select来达到多个channel(可同时接受和发送)复用。
下面给个简单例子:
func main() {
event := make(chan string)
timeout := make(chan bool, 1)
go func() {
time.Sleep(1 * time.Second) // 休眠1s,如果超过1s还没I操作则认为超时,通知select已经超时啦~
timeout <- true
}()
go func() {
event <- "go happens"
}()
DONE:
for {
select {
case <-event:
fmt.Println("happens!")
//goto DONE
break DONE
case <-timeout:
fmt.Println("Timeout!")
default:
fmt.Println("No news, please wait.")
time.Sleep(2 * time.Second)
}
}
//DONE:
fmt.Println("return!")
}