前言
先来看看Rob Pick关于Golang并发的哲学
不要通过共享内存来通信,而应该通过通信来共享内存
下面我仅以我个人观点来描述我对这句话的理解:
Golang不同于其他语言使用线程(比如Java)实现并发处理,它使用了更为轻量级的goroutine(协程来处理并发)。 \
要理解「不要通过共享内存来通信」这句话,我们以Java中的并发举例
Java中多线程来并发操作一块临界资源时,本质上就是通过操作的一块共享资源,多线程之间通过这块共享资源进行通信。多线程之间需要依靠同步锁、信号量等同步机制来保证数据的正确性
而Golang中的channel正好是「而应该通过通信来共享内存」的完美诠释。
在Golang中我们可以把处于临界资源的数据放入channel,多个goroutine可以并发安全的中channel写入和读取数据(Golang语言层面支持)。Golang使用channel来协调多个goroutine进行通信,从而达到能够共享内存的目的
通道的使用
func main() {
ch1 := make(chan int, 2)
ch1 <- 1
ch1 <- 2
fmt.Println(<-ch1) // 输出1
fmt.Println(<-ch1) // 输出2
}
ch1 := make(chan int, 2):
我使用make初始化了一个int类型的的通道ch1(int 限制了该通道能够存储的数据类型),并为了指定了通道的容量(该通道能够存储的元素个数)。
ch1 <- 1
ch1 <- 2
这两行代码的作用是往通道ch1中添加了两个元素,通过输出语句我们可以看出通道其实类似一个FIFO(先进先出)队列。在使用 「<- 通道名」取出通道元素时是按照放入的顺序来取的
缓冲通道
当我们创建通道时如果指定了通道的容量,那么该通道即为缓冲通道。
func main() {
ch1 := make(chan int, 2)
producer := func() {
index := 0
for {
ch1 <- index
fmt.Println("producer:", index)
time.Sleep(time.Second)
index++
}
}
consumer := func() {
for {
fmt.Println("consumer:", <-ch1)
time.Sleep(time.Second * 2)
}
}
go producer()
go consumer()
// 不让主协程退出
for {
}
}
缓冲通道和多协程搭配使用可以实现异步处理
非缓冲通道
当创建通道时如果没有指定通道容量或者指定通道容量为0,那么该通道即为非缓冲通道。对于非缓冲通道无论是单独的写入还是读取都对造成当前协程的阻塞,除非与之对应的操作开始执行后,当前协程才会解除阻塞。由此可见非缓冲通道是使用同步的方式来传递数据
ch1 := make(chan int, 0)
ch1 := make(chan int)
通道阻塞
func main() {
ch1 := make(chan int, 2)
ch1 <- 1
ch1 <- 2
// ch1 <- 3 // 通道已满,此处会造成主程序阻塞,程序会panic
// ch2 := make(chan int, 1)
// fmt.Println(<-ch2) // 通道为空,此处会造成主程序阻塞,程序会panic
// var ch3 chan int
// ch3 <- 1 // 对值为nil通道的写操作会阻塞,程序会panic
// fmt.Println(<-ch3) // 对值为nil通道的读操作会则色,程序会panic
}
如上代码所示,在使用通道造成阻塞场景大致有三种
- 往已经满的通道中写数据会造成当前协程阻塞
- 从空的通道中读取数据会造成当前协程阻塞
- 读写值为nil的通道也会造成协程阻塞
错误使用通道示例
- 对已经关闭的通道进行写操作
func main() {
ch1 := make(chan int, 2)
close(ch1)
ch1 <- 1 // panic: send on closed channel
}
- 对已经关闭的通道再进行关闭
func main() {
ch1 := make(chan int, 2)
close(ch1)
close(ch1) // panic: close of closed channel
}