Go语言提供的协程(goroutine)来进行并发编程,协程是轻量级线程,对资源消耗相对线程来说比较小,一个程序中可以同时运行成千上万个协程。如同多线程编程,多个协程之间通常需要访问共享资源,例如访问共享的内存数据。常规的方式是使用互斥锁来安全的访问共享资源,Go语言也提供了传统的同步机制,即Go标准库sync中的sync.Mutex。sync.Mutex类型有两个方法:Lock和Unlock,Lock锁定当前的共享资源,Unlock进行解锁。另外Go也提供了一种独特的并发同步技术:通道(Channel),通道可用于两个协程之间安全的传递数据。Go语言提倡使用通道的方法来访问共享内存,当一个资源需要在协程之间共享时,通道在协程之间架起了一个管道,并提供了确保同步交换数据的机制。可以将一个通道视为程序内部的FIFO(先进先出)的队列。一些协程发送值到队列,其他协程从队列中获取值。
像数组、切片和map一样,每个通道都有一个元素类型,也就是通道可发送数据的类型。声明通道时,需要指定将要被共享的数据的类型。
通道是引用类型,需要使用 make 进行创建,格式如下:
通道实例 := make(chan 数据类型)
ch1 := make(chan int)//创建一个整型类型的通道
ch2 := make(chan interface{})//创建一个空接口类型的通道, 可以存放任意格式
通道的发送使用特殊的操作符<-,将数据通过通道发送的格式为:
通道变量 <- 值
//创建一个整型类型的通道
ch := make(chan int)
//将0放入通道中
ch <- 5
一个通道发送和接收数据,默认是阻塞的。当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个协程从该通道读取数据。例如:下面的代码执行将会报错:
package main
func main() {
ch := make(chan int)
ch <- 5
}
//报错:
//fatal error: all goroutines are asleep - deadlock!
通道接收使用<-操作符,从通道中接收数据的格式为:
变量 <- 通道变量
package main
import "fmt"
func sum(arr []int, c chan int) {
sum := 0
for _, v := range arr {
sum += v
}
c <- sum //把sum发送到通道c
}
func main() {
arr := []int{1, 2, 3, 4, 5, 6}
c := make(chan int)
go sum(arr, c)
x:= <-c //从通道c中接收数据
fmt.Println(x)
}
通道接收有如下特性:
for data := range ch {
}
通道默认是双向的,但有时候需要限制一个通道只可以接收,但是不能发送,有时候限制一个通道只能发送,但是不能接收,这种通道我们称为单向通道。
定义单向通道也很简单,只需要在定义的时候,带上<-即可:
var sendch chan<- int //只能发送
var receive <-chan int //只能接收
单向通道常应用于函数或者方法的参数,比如:
func sendData(sendch chan<- int) {
sendch <- 10
}
尽管可用make创建单向通道,但通常使用类型转换来获取单向通道,并分别赋予操作双方,例如:
package main
import "fmt"
func sendData(sendch chan<- int) {
sendch <- 10
}
func main() {
chnl := make(chan int)
go sendData(chnl)
fmt.Println(<-chnl)
}
缓冲通道就是指通道带有一个缓冲区,发送数据到一个缓冲通道只有在缓冲区满时才被阻塞。类似地从缓冲通道接收时只有在缓冲区为空才会被阻塞。创建缓冲通道的格式为:
通道实例 := make(chan 通道类型, 缓冲大小)
例如:
package main
import "fmt"
func main() {
//创建一个3个元素缓冲大小的整型通道
ch := make(chan int, 3)
//查看当前通道的大小
fmt.Println(len(ch))
// 发送3个整型元素到通道
ch <- 1
ch <- 2
ch <- 3
// 查看当前通道的大小
fmt.Println(len(ch))
}
select是Go中的一个控制结构,类似于switch语句,可用于处理异步IO操作。
select会监听case语句中通道的读写操作,当case中通道读写操作为非阻塞状态(即能读写)时,将会触发相应的动作。
如果有多个case都可以运行,select会随机选出一个执行。如果没有可运行的case语句,且没有default语句,select将阻塞,直到某个case通信可以运行。Go中的select语句格式如下:
select {
case <-ch1: // 如果从 ch1 信道成功接收数据,则执行该分支代码
case ch2 <- 1: // 如果成功向 ch2 信道成功发送数据,则执行该分支代码
default: // 如果上面都没有成功,则进入 default 分支处理流程
}
可以看到select的语法结构有点类似于switch,但又有些不同。select里的case后面并不带判断条件,而是一个信道的操作。
Go的select就是监听IO操作,当IO操作发生时,触发相应的动作。每个case语句里必须是一个IO操作,确切的说,应该是一个面向通道的IO操作。
package main
import "time"
import "fmt"
func main() {
//创建两个通道
c1 := make(chan string)
c2 := make(chan string)
go func(c chan string) {
time.Sleep(time.Second * 1)
c <- "hello"
}(c1)
go func(c chan string) {
time.Sleep(time.Second * 2)
c <- "world"
}(c2)
//我们使用select来等待这两个通道的值
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Printf("c1's data:%s\n", msg1)
case msg2 := <-c2:
fmt.Printf("c2's data:%s\n", msg2)
}
}
}
//输出结果:
//c1's data:hello
//c2's data:world
如我们所期望的,程序输出了正确的值。对于select语句而言,它不断地检测通道是否有值过来,一旦发现有值过来,立刻获取输出。
如果发送方知道没有更多的数据要发送,告诉接收者所在的协程可以停止等待是很有用的。可以通过调用内置的Close函数来关闭通道:
close(ch)
接收器可以在从通道接收数据时使用附加变量来检查通道是否已经关闭:
v, ok := <-ch
在通道关闭后,任何后续的发送操作将会导致应用崩溃。当关闭的通道被读完(就是最后一个发送的值被接收)后,所有后续的接收操作都会立即返回,返回值是对应类型的零值。
package main
import "fmt"
func main() {
c := make(chan int, 3)
c <- 10
c <- 20
close(c)
for i := 0; i < 3; i++ {
x, ok := <-c
fmt.Println(i, ":", ok, x)
}
}
//输出结果:
//0 : true 10
//1 : true 20
//2 : false 0
关闭通道还可以作为一个广播机制。当一个通道关闭且已经取完所有发送的值后,接下来的接收操作都会立刻返回,得到零值。就可以利用这个特性来创建一个广播机制:
package main
import "fmt"
func main() {
jobs := make(chan int, 5)
done := make(chan bool)
//这是工作协程
go func() {
for {
job, ok:= <-jobs
if ok {
fmt.Println("收到任务", job)
} else {
fmt.Println("收到任务结束通知")
close(done)//关闭通道,通知main继续运行
return
}
}
}()
//发送任务给工作协程
for i := 1; i <= 3; i++ {
jobs <- i
fmt.Println("发送任务", i)
}
close(jobs)//关闭通道,通知工作协程退出
fmt.Println("发送任务结束通知")
<-done //阻塞,直到数据或通道关闭
}
输出结果:
发送任务 1
发送任务 2
发送任务 3
发送任务结束通知
收到任务 1
收到任务 2
收到任务 3
收到任务结束通知