一、前言
传统的线程模型,比如经常使用Java、C++、Python编程的时候,需要多个线程之间通过共享内存(比如在堆上创建的共享变量)来通信。这时候为保证线程安全,多线程共享的数据结构需要使用锁来保护,多线程访问共享数据结构时候需要竞争获取锁,只有获取到锁的线程才可以存取共享数据。这种情况下使开发人员更加容易的使用线程安全的数据结构,比如Java中JUC包中的并发安全的队列、列表等。
前面章节我们讲解了Go中提供的低级同步原语-锁,其实Go的并发原语 - goroutines和channels 提供了一种优雅而独特的结构化开发并发软件的方式。Go鼓励使用通道在goroutine之间传递对共享数据的引用,而不是明确地使用锁来保护对共享数据的访问。这种方法确保在给定时间只有一个goroutine可以访问共享数据。这个理念被总结为:不要通过共享内存来通信,而要通过通信来共享内存。
本章我们就来讲解有关Channel的知识,我们可以把通道理解为一个并发安全的队列,生产者goroutine可以向通道里面放入元素,消费者goroutine可以从通道里面获取元素。
从队列大小来看通道可以分为有缓冲通道和无缓冲通道,无缓冲通道里面最多有一个元素,有缓冲通道里面可以有很多元素。
另外通道还是有方向的,如果一个通道只允许向通道里面放元素,但是不允许从通道里面取元素,则我们称之为单向的发送通道(向通道里面写元素),例如var ch chan <-int声明了一个发送通道;如果一个通道只允许从通道里面获取元素,而不允许向其中写入元素,则称之为接受通道(从通道里面读取元素),例如var ch <-chan int声明了一个接受通道;如果一个通道既可以从中读取元素,又可以向其中写入元素,则称之为双向通道,例如var ch chan int声明了一个双向通道。
另外通道是可以被关闭的,当调用close(ch)关闭了通道ch后,不能再向通道ch写入元素,但是可以从通道读取元素。
二、无缓冲通道
在go中创建一个无缓冲的通道使用下面两种方式:
var c chan int
c = make(chan int)
或者
c := make(chan int)
如上创建了一个int类型的无缓冲通道c,其中第一种方式是先声明,然后在初始化;第二种是简短式声明和初始化一步完成,也就推荐的方式;
向通道c内写入获取读取元素可以使用<-符号,比如向通道c写入元素12,c<-12;从通道c中读取元素可以使用<-c,比如从通道读取一个元素到变量w,w := <-c;当没有向通道内写入元素时候,试图从通道内读取元素的goroutine会被阻塞;对应无缓冲通道,当试图向没有goroutine正在从通道读取元素的通道写入元素时候,写入的goroutine会被阻塞。
需要注意的是这里的make和chan都是内置的语言层面的关键字,当我们创建具体类型的通道时候只需要替换int就可以了。
下面我们看个例子:
package main
import (
"fmt"
"sort"
)
var c chan int
var nums []int
func main() {
//1.初始化通道
c = make(chan int)
nums = []int{5, 3, 7, 2, 9, 1, 6}
//2.开启goroutine排序
go func() {
//2.1
sort.Ints(nums)
//2.2
c <- 1
}()
//3.阻塞,直到通道内有元素
<-c
fmt.Println(nums)
}
如上首先创建了一个无缓冲通道c,和一个切片nums
代码2开启了一个goroutine1
代码3企图从通道内读取一个元素,当通道内没有元素时候,代码3所在的 goroutine就会被阻塞,goroutine1执行完毕2.1对元素排序后,会执行代码2.2 向通道写入一元素,这时候代码3就会返回,然后打印排序后的切片内容
这里我们使用无缓冲通道实现了之前使用WaitGroup来实现主goroutine等待子goroutine执行完毕的方式。
三、有缓冲通道
在go中创建一个有缓冲的通道使用下面两种方式:
var c chan int
c = make(chan int, 1)
或者
c := make(chan int, 1)
如上创建了一个int类型的缓冲队列为1通道c,其中第一种方式是先声明,然后在初始化;第二种是简短式声明和初始化一步完成,也就推荐的方式;
有缓冲通道当缓冲有空间时候,向里面放入元素会马上返回,当缓冲满了的时候在放入元素调用goroutine会被阻塞;当通道内没有元素时候尝试从通道获取元素会被阻塞。
下面看一个使用有缓冲通道实现生产消费模型:
package main
import (
"fmt"
)
func printer(ch <-chan int, wg chan<- int) {
//3.1
for i := range ch {
fmt.Println(i)
}
//3.2
wg <- 1
close(wg)
}
func main() {
//1.创建缓冲通道
ch := make(chan int, 10)
//2.创建同步用的无缓冲通道
wg := make(chan int)
//3.开启go协程
go printer(ch, wg)
//4.写入到通道
for i := 1; i < 100; i++ {
ch <- i
}
//5.关闭协程
close(ch)
fmt.Println("wait sub goroutine over")
//6.等待子goroutine结束
<-wg
fmt.Println("main goroutine over")
}
如上代码1创建另一个含有10个int类型的元素的有缓冲通道ch,代码2创建了一个无缓冲通道wg用来做线程间同步
代码3开启新goroutine执行函数printer,其内部从通道ch读取元素,一开始ch内没有元素,则当前goroutine会被阻塞
代码4则向通道ch写入100个元素,当ch里面有元素时候,新goroutine就会被激活,然后从通道里面跌打出元素进行打印
代码5则当向ch写入100个元素后关闭通道,关闭后不能再向通道写入元素,但是通道内的元素还是会被读取的,代码6则试图从通道wg读取元素,一开始通道内无元素 所以main groutine阻塞到这里。
等新goroutine代码3.1把通道ch里面元素全部迭代完毕后,执行代码3.2向通道wg写入一个元素然后关闭通道,这时候main goroutine则会从代码6中返回。
Go中以消息进行通信的方式允许程序员安全地协调多个并发任务,并且容易理解语义和控制流,这通常比其他语言比如Java中的回调函数(callbacks)或共享内存方式更优雅简单。