介绍
在「Effective Go」并发章节讲到,“不要通过共享内存进行通信;而是通过通信共享内存”。由此表明 Golang 语言官方鼓励用户使用“通过通信共享内存”的方式并发编程。
但是,Golang 语言也在标准库 sync 包中提供了传统的同步原语。
我们应该选择哪种并发编程方式呢?Golang 语言官方也给了使用指南:
Channel | Mutex |
passing ownership of data, distributing units of work, communicating async results |
caches, state |
如表格所示,传递数据的所有权,分发工作任务和通信异步结果,这三种场景建议使用 Channel。缓存和状态,这两种场景建议使用 Mutex。
在 Golang 语言官方博客「The Go Memory Model」一文中,对 channel 的描述如下:
A send on a channel happens before the corresponding receive from that channel completes.
A receive from an unbuffered channel happens before the send on that channel completes.
本文我们主要介绍一些关于 Channel 的使用方式。关于 Golang 语言 sync 包的同步原语和 Channel 的介绍,我们在之前的文章已经介绍过了,在此不再赘述,感兴趣的读者朋友可以按需翻阅。
02
无缓冲 channel
无缓冲 channel 可用于两个 goroutine 之间传递信号,比如以下示例:
顺序打印 1 至 100 的奇数和偶数:
func main () { block := make(chan struct{}) go odd(block) go even(block) time.Sleep(time.Second) fmt.Println("done") } func odd (block chan struct{}) { for i := 1; i <= 100; i++ { <-block if i % 2 == 1 { fmt.Println("奇数:", i) } } } func even (block chan struct{}) { for i := 1; i <= 100; i++ { block <- struct{}{} if i % 2 == 0 { fmt.Println("偶数:", i) } } }
阅读上面这段代码,我们使用一个无缓冲 channel 作为两个 goroutine 之间的信号传递的桥梁。
主子 goroutine 之间传递信号:
func main () { block := make(chan struct{}) go func() { for i := 0; i < 10; i++ { fmt.Println(i) } // block <- struct{}{} close(block) }() <-block fmt.Println("done") }
阅读上面这段代码,我们使用一个无缓冲 channel 作为主子 goroutine 之间的信号传递的桥梁。通过信号传递,主 goroutine 在子 goroutine 运行结束之后再退出。
03
有缓冲 channel
有缓冲 channel 可以用于解耦操作,模拟消息队列。“生产者”和“消费者”只需各自处理 channel,实现解耦。
解耦“生产者”和“消费者”:
func main () { task := make(chan int, 10) go consumer(task) // 生产者 for i := 0; i < 10; i++ { task <- i } time.Sleep(time.Second * 2) } // 消费者 func consumer (task <-chan int) { for i := 0; i < 10; i++ { go func(id int) { t := <- task fmt.Println(id, t) }(i) } }
阅读上面这段代码,我们使用一个有缓冲的 channel,将“生产者”和“消费者”做解耦操作。
04
超时操作和定时器
我们还可以通过 select 和 channel,实现超时操作和定时器。
超时操作:
func main() { c1 := make(chan string, 1) go func() { time.Sleep(2 * time.Second) c1 <- "result 1" }() select { case res := <-c1: fmt.Println(res) case <-time.After(1 * time.Second): fmt.Println("timeout 1") } c2 := make(chan string, 1) go func() { time.Sleep(2 * time.Second) c2 <- "result 2" }() select { case res := <-c2: fmt.Println(res) case <-time.After(3 * time.Second): fmt.Println("timeout 2") } }
阅读上面这段代码,我们通过 c1 和 c2 两个 channel,分别模拟出超时和未超时场景。
定时器:
func main() { ticker := time.NewTicker(500 * time.Millisecond) done := make(chan bool) go func() { for { select { case <-done: return case t := <-ticker.C: fmt.Println("Tick at", t) } } }() time.Sleep(1600 * time.Millisecond) ticker.Stop() done <- true fmt.Println("Ticker stopped") }
阅读上面这段代码,我们定义一个打点器,每间隔 500ms 执行一次操作,当打点器 stop 时,通过一个无缓冲 channel 传递退出信号。
05
总结
本文我们介绍了一些关于 Channel 的使用方式,我们在阅读完本文后可以了解无缓冲 channel 作为信号传递的使用方式和有缓冲 channel 解耦操作的方式,以及 channel 与 select 配合使用的用法。
推荐阅读:
参考资料:
https://golang.org/doc/effective_go#concurrency
https://github.com/golang/go/wiki/MutexOrChannel
https://tip.golang.org/ref/mem#tmp_3
https://gobyexample.com/timeouts
https://gobyexample.com/tickers