01
介绍
我们在之前的文章中介绍过 golang 使用 sync 和 context 并发编程。我在文末给出相关文章的链接,还没有阅读的读者推荐阅读一下。
golang 作者 Rob Pike 说过一句话,不要通过共享内存来通信,而应该通过通信来共享内存。怎么通过通信来共享内存呢?使用 channel 可以实现 Rob Pike 说的通过通信来共享内存,我们可以使用 channel 在多个 goroutine 中传递数据。今天我们来介绍一下 golang 使用 channel 并发编程,在介绍并发编程前,先介绍一下 channel。
channel 是并发安全的类型,同一时刻,运行时只会执行一个对同一 channel 操作(发送或接收)中的某一个操作(发送或接收),即操作(发送或接收)之间是互斥的。并且对同一 channel 中的同一个元素执行的发送和接收操作之间也是互斥的。
02
无缓冲和有缓冲 channel
在 golang 中,声明 channel 需要使用 make 函数,第一个参数是 chan int,它是 channel 中数据的数据类型,第二个参数是可选参数 cap,它是一个 int 类型的参数,且值必须大于 0。如果没有给定第二个参数,该 channel 为无缓冲 channel。反之为有缓冲 channel。
// 声明一个 int 类型的无缓冲 channel c1 := make(chan int) // 声明一个 int 类型的有缓冲 channel,容量 cap 为 5 c2 := make(chan int, 5)
03
channel 接收和发送
为了便于理解,我们可以将 channel 理解为一个 FIFO 先进先出的队列,sender 向 channel 中发送数据,receiver 从 channel 中接收数据。发送和接收使用的操作符都是 <-
,它们的区别是,发送时操作符在 channel 类型变量名的右边,接收时操作符在 channel 类型变量的左边。
c := make(chan int, 2) // send c <- 1 c <- 2 // receive <- c // 接收并赋值给变量 x <- c
04
channel 关闭
使用 close 函数可以关闭 channel,channel 关闭后,发送方继续向 channel 中发送数据会引发 panic,但是接收方可以感知到 channel 关闭,并且可以安全退出。
注意:关闭一个已经关闭的 channel,也会引发 panic。
我们可以通过接收第二个返回值,判断当前 channel 是否关闭。
c := make(chan int, 5) close(c) val, ok := <- c fmt.Println(val, ok)
第二个返回值为 true,表示 channel 未关闭,反之表示已关闭。
05
单向 channel
我们以上讲述的都是双向 channel,即可以向 channel 发送数据,也可以从 channel 接收数据。此外,还有单向 channel,即只可以对 channel 进行发送或接收数据的操作。
// 单向 channel,只能发送不能接收(发送 channel) c1 := make(chan<- int, 5) // 单向 channel,只能接收不能发送(接收 channel) c2 := make(<-chan int, 5)
可能有读者要问了,channel 不就是为了传递数据的吗?单向 channel 只能发送或只能接收,无法传递数据,有什么意义吗?
是的,单向 channel 主要用于约束的作用。
// 返回一个 int 类型的单向 channel(接收 channel) func receiver () <-chan int { // 声明一个 int 类型的有缓冲 channel,容量是 5 c := make(chan int, 5) // 向 channel 中发送数据 for i := 0; i < 5; i++ { c <- i } // 关闭 channel close(c) return c } func main () { c1 := receiver() for i := range c1 { fmt.Println(i) } }
阅读这段代码,我们声明一个 receiver 函数,它的返回值是一个接收 channel。main 函数中,通过调用 receiver 函数,声明一个 <-chan int
类型的 channel 变量 c1。并且使用 for ... range
遍历 c1。
06
使用 channel 并发编程
前面的内容,我们主要介绍了使用 channel 在多个 goroutine 之间进行通信,本小节我们介绍使用 channel 在多个 goroutine 之间进行同步。channel 是 golang 提供的基于 CSP (Communicating Sequential Process)的并发原语,我们可以使用 channel 并发编程。
// 任务 1 func firstTask () int { time.Sleep(time.Millisecond * 100) fmt.Println("first task") return 1 } // 任务 2 func secondTask() { fmt.Println("second task start") time.Sleep(time.Millisecond * 200) fmt.Println("second task end") } // 执行任务 1 func runTask() chan int { c := make(chan int) go func() { val := firstTask() c <- val }() return c } // 并发执行任务 1 和任务 2 func main () { // 执行任务 1 c := runTask() // 执行任务 2 secondTask() fmt.Println(<- c) }
阅读上面这段代码,使用 channel 并发编程。为了避免和 sync 并发编程混淆,特意使用 time.Sleep() 替代 sync.WaitGroup。
代码中的 runTask 函数,返回值是 chan int
,被用于两个 goroutine 之间的通知信号。该信号用于通知 main goroutine,runTask 函数所在的 goroutine,先调用 firstTask 函数,并被阻塞 100 毫秒,main goroutine 先执行 secondTask 函数,当 runTask 函数返回值赋值给变量 c 后,main goroutine 打印输出变量 c 的值。
07
select 多路选择和 time.After 超时
select 语句的用法类似于 switch 语句,但是 select 语句只能用于操作 channel。
func firstCh () <-chan int { c := make(chan int, 1) go func() { time.Sleep(time.Millisecond * 200) c <- 1 }() return c } func secondCh () <-chan int { c := make(chan int, 1) go func() { time.Sleep(time.Millisecond * 300) c <- 2 }() return c } func main() { select { case val, ok := <- firstCh(): if !ok { fmt.Println("channel is closed") break } fmt.Println("first channel success", val) case val, ok := <- secondCh(): if !ok { fmt.Println("channel is closed") break } fmt.Println("second channel success", val) // case <-time.After(time.Millisecond * 100): // fmt.Println("time out") // default: // fmt.Println("default") } }
阅读上面这段代码,我们使用 golang 并发原语 channel,结合 select 实现多个 goroutine 之间同步。
08
总结
本文我们介绍了 channel 在 golang 中的相关操作,和使用 channel 并发编程,即通过通信来共享内存的方式。其中使用 channel 并发编程内容中,是通过将 channel 作为 goroutine 之间的通知信号,此外,还可以通过 channel 替代锁。限于篇幅,关于 channel 替代锁的内容留给大家自己去实现。
推荐阅读:
参考资料:
https://golang.org/doc/codewalk/sharemem/
https://tour.golang.org/concurrency/2
https://gobyexample.com/channels