换做是 go 的实现:
func main() { notify := make(chan struct{}) for i := 0; i < 10; i++ { go func(i int) { for { select { case <-notify: fmt.Println("done.......",i) return case <-time.After(1 * time.Second): fmt.Println("wait notify",i) } } }(i) } time.Sleep(1 * time.Second) close(notify) time.Sleep(3 * time.Second) }当关闭一个 channel 后,会使得所有获取 channel 的 goroutine 直接返回,不会阻塞,正是利用这一特性实现了广播通知所有 goroutine 的目的。
注意,同一个 channel 不能反复关闭,不然会出现panic。
channel 解耦以上例子都是基于无缓冲的 channel,通常用于 goroutine 之间的同步;同时 channel 也具备缓冲的特性:
ch :=make(chan T, 100)可以直接将其理解为队列,正是因为具有缓冲能力,所以我们可以将业务之间进行解耦,生产方只管往 channel 中丢数据,消费者只管将数据取出后做自己的业务。
同时也具有阻塞队列的特性:
当 channel 写满时生产者将会被阻塞。
当 channel 为空时消费者也会阻塞。
从上文的例子中可以看出,实现相同的功能 go 的写法会更加简单直接,相对的 Java 就会复杂许多(当然这也和这里使用的偏底层 api 有关)。
Java 中的 BlockingQueue这些特性都与 Java 中的 BlockingQueue 非常类似,他们具有以下的相同点:
可以通过两者来进行 goroutine/thread 通信。
具备队列的特征,可以解耦业务。
支持并发安全。
同样的他们又有很大的区别,从表现上看:
channel 支持 select 语法,对 channel 的管理更加简洁直观。
channel 支持关闭,不能向已关闭的 channel 发送消息。
channel 支持定义方向,在编译器的帮助下可以在语义上对行为的描述更加准确。
当然还有本质上的区别就是 channel 是 go 推荐的 CSP 模型的核心,具有编译器的支持,可以有很轻量的成本实现并发通信。
而 BlockingQueue 对于 Java 来说只是一个实现了并发安全的数据结构,即便不使用它也有其他的通信方式;只是他们都具有阻塞队列的特征,所有在初步接触 channel 时容易产生混淆。
相同点 channel 特有阻塞策略 支持select
设置大小 支持关闭
并发安全 自定义方向
普通数据结构 编译器支持
总结
有过一门编程语言的使用经历在学习其他语言是确实是要方便许多,比如之前写过 Java 再看 Go 时就会发现许多类似之处,只是实现不同。
拿这里的并发通信来说,本质上是因为并发模型上的不同;
Go 更推荐使用通信来共享内存,而 Java 大部分场景都是使用共享内存来通信(这样就得加锁来同步)。
带着疑问来学习确实会事半功倍。
最近和网友讨论后再补充一下,其实 Go channel 的底层实现也是通过对共享内存的加锁来实现的,这点任何语言都不可避免。
既然都是共享内存那和我们自己使用共享内存有什么区别呢?主要还是 channel 的抽象层级更高,我们使用这类高抽象层级的方式编写代码会更易理解和维护。
但在一些特殊场景,需要追求极致的性能,降低加锁颗粒度时用共享内存会更加合适,所以 Go 官方也提供有 sync.Map/Mutex 这样的库;只是在并发场景下更推荐使用 channel 来解决问题。