goruntine和channel
前面几个回合,我们都是实例,但在go中有个很常用的东东,我们代码使用的不多,这一次我们单独拿来讲讲。
goroutine
goroutine是go语言特有的并发体,是一种轻量级的线程,由go关键字启动。
goroutine采用的是半抢占市的协作调度,只有当前goroutine发生阻塞时才会导致调度(也就是goroutine的切换)。
那么项目中使用了多个goroutine,如何在不同的goroutine之间通信呢?
学习go的过程中,想必你应该知道有这么一个经典的句子:
Do not communicate by sharing memory; instead, share memory by communicating.
goroutine的通信用通道
通道是个啥?
一个通道相当于一个先进先出(FIFO)的队列,通道中的各个元素是严格地按照发送的顺序排列的,先被发送通道的一定会先被接收。元素的发送和接收都需要用到操作符
<-
。
这里有一个FIFO简单的例子
package main
import "fmt"
func main() {
ch := make(chan int, 5)
ch <- 1
ch <- 2
ch <- 3
ch <- 4
ch <- 5
fmt.Println("1-", <-ch)
fmt.Println("2-", <-ch)
ch <- 6
fmt.Println("3-", <-ch)
fmt.Println("4-", <-ch)
fmt.Println("5-", <-ch)
fmt.Println("6-", <-ch)
close(ch)
}
/*
打印的内容如下:
1- 1
2- 2
3- 3
4- 4
5- 5
6- 6
*/
我们再来看一下通道的常规操作:
创建通道
通道(channel)分两种(容量是否为0):
- 缓冲通道
- 非缓冲通道
// 缓冲通道
ch1 := make(chan int, 10)
ch2 := make(chan bool, 2)
// 非缓冲通道
ch3 := make(chan int)
ch4 := make(chan bool, 0)
发送通道数据
// 创建一个空接口通道,注意定义的通道类型有
ch := make(chan interface{})
// 将0放入通道中
ch <- 0
// 将hello字符串放入通道中
ch <- "hello"
接收通道数据
1. 阻塞接收数据
阻塞模式接收数据时,将接收变量作为<-
操作符的左值,格式如下:
data := <-ch
执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。
2. 非阻塞接收数据
使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:
data, ok := <-ch
// data:表示接收到的数据。未接收到数据时,data 为通道类型的零值
// ok:表示是否接收到数据。
非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel 进行,可以参见后面的内容。
3. 接收任意数据,忽略接收的数据
阻塞接收数据后,忽略从通道返回的数据,格式如下:
<-ch
4. 循环接收
通道的数据接收可以借用 for range 语句进行多个元素的接收操作,格式如下:
package main
import (
"fmt"
"time"
)
func main() {
// 构建一个通道
ch := make(chan int)
// 开启一个并发匿名函数
go func() {
// 从3循环到0
for i := 3; i >= 0; i-- {
// 发送3到0之间的数值
ch <- i
// 每次发送完时等待
time.Sleep(time.Second)
}
}()
// 遍历接收通道数据
for data := range ch {
// 打印通道数据
fmt.Println(data)
// 当遇到数据0时, 退出接收循环
if data == 0 {
break
}
}
}
关闭通道
ch := make(chan string)
...
close(ch)
通道特性
- 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的(并发安全)
- 发送操作和接收操作中对元素值的处理都是不可分割的。
- 发送操作在完全完成之前会被阻塞,接收操作也是一样。
- 对于缓冲通道:如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走;如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。
- 对于非缓冲通道:无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。
注意点:
- 关闭通道要在发送方关闭,关闭后如果channel内还有元素,并不会对接下来的接收产生影响
- 单向通道最主要的用途就是约束其他代码的行为
- 通过函数的参数类型或者返回值类型来限制(Go的语法糖)。
func(ch chan<- int);传入双向通道,在函数里面调用ch只能发送
func() (ch <-chan int);返回双向通道,在函数外面里面调用ch只能接收
说到这里,我们提一下channel独有的关键字——select。
A “select” statement chooses which of a set of possible send or receive operations will proceed. It looks similar to a “switch” statement but with the cases all referring to communication operations.
一个select语句用来选择哪个case中的发送或接收操作可以被立即执行。它类似于switch语句,但是它的case涉及到channel有关的I/O操作。
说完这些概念性的玩意儿,我们还是来几个实例,感受一下goroutine配合channel使用的快感。
chan配合select实现超时处理
func main() {
timeout := make(chan bool)
go func() {
time.Sleep(3e9)
timeout <- true
}()
ch := make(chan int)
select {
case <-ch:
case <-timeout:
fmt.Println("timeout!")
}
}
非缓冲通道,监听信号量
// 来自gin文档的例子
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
// 服务连接
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
log.Println("Server exiting")
}
当然还有其他应用场景,如消息传递、消息过滤,事件订阅与广播,请求、响应转发,并发控制,同步与异步等,可参考下面的文章:
实例代码点击见githubgithub.com/puresai/go-example/tree/main/demo8-goroutine-channel
参考: