package main
import"fmt"funcmain(){
fmt.Println("main start")// main 函数的第一个语句是打印 main start 到控制台。
channel :=make(chanstring)// 在 main 函数中使用 make 函数创建一个 string 类型的通道赋值给 ' channel ’ 变量
channel <-"GoLang"// 给通道 channel 传入一个数据 DEMO.// 此时主线程将阻塞直到有协程接收这个数据. Go 的调度器开始调度协程接收通道 channel 的数据// 但是由于没有协程接受,没有协程是可被调度的。所有协程都进入休眠状态,即是主程序阻塞了。
fmt.Println("main stop")}/*
报错
main start
fatal error: all goroutines are asleep - deadlock! //所有协程都进入休眠状态,死锁
goroutine 1 [chan send]:
main.main()
*/
6.3 关闭通道
package main
import"fmt"funcRushChan(c chanstring){<- c
fmt.Println("1")<- c
fmt.Println("2")}funcmain(){
fmt.Println("main start")
c :=make(chanstring,1)goRushChan(c)
c <-"Demo1"close(c)/*
不能向一个关了的channel发信息
main start
panic: send on closed channel
*/
c <-"Demo2"//close(c)/*
close 放这里的话可以
main start
1
2
Main Stop
*/
fmt.Println("Main Stop")}
第一个操作 c <- "Demo2" 将阻塞协程直到有其他协程从此通道中读取数据,因此 greet 会被调度器调度执行。
package main
import"fmt"funcRushChan(c chanstring){for{
val ,_:=<-c
fmt.Println(val)}}funcmain(){
fmt.Println("Main Start")
c :=make(chanstring,1)goRushChan(c)
c <-"Demo1"//结果1//c <- "Demo2" //结果2
fmt.Println("Main Stop")}/*
结果1:
Main Start
Main Stop
*//*
结果2:
Main Start
Join
Mike
Main Stop
*/
package main
import"fmt"funcRushChan(c chanstring){for{
val ,_:=<-c
fmt.Println(val)}}funcmain(){
c :=make(chanint,3)
c <-1
c <-2
c <-3close(c)for elem :=range c {
fmt.Println(elem)}}
这里虽然关闭了通道,但是其实数据不仅在通道里面,数据还在缓冲区中的,我们依然可以读取到这个数据。
6.5 通道的长度和容量
和切片类似,一个缓冲通道也有长度和容量。 通道的长度是其内部缓冲队列未读的数据量,而通道的容量是缓冲区可最大盛放的数据量。 我们可以使用 len 函数去计算通道的长度,使用 cap 函数去获得通道的容量。和切片用法神似
package main
import"fmt"funcRushChan(c chanstring){for{
val ,_:=<-c
fmt.Println(val)}}funcmain(){
c :=make(chanint,3)
c <-1
c <-2
fmt.Println("长度: ",len(c))
fmt.Println(<-c)
fmt.Println("长度: ",len(c))
fmt.Println(<-c)
fmt.Println("长度: ",len(c))
fmt.Println("容量: ",cap(c))}/*
结果:
长度: 2
1
长度: 1
2
长度: 0
容量: 3
*/
但是假如你在一个协程中只需要读操作某通道,但是在主线程中却需要读写操作这个通道该怎么办呢? 幸运的是 Go 提供了一个简单的语法去把双向通道转化为单向通道。
package main
import"fmt"funcgreet(roc <-chanstring){
fmt.Println("Hello "+<-roc ,"!")}funcmain(){
fmt.Println("Main Start")
c :=make(chanstring)gogreet(c)
c <-"Demo"
fmt.Println("Main Stop")}/*
结果
Main Start
Hello Demo !
Main Stop
*/
我们修改 greet 协程函数,把参数 c 类型从双向通道改成单向接收通道。
现在我们只能从通道中读取数据,通道上的任何写入操作将会发生错误: “invalid operation: roc <- “Temp” (send to receive-only type <-chan string)”.
6.7 Select
select 和 switch 很像,它不需要输入参数,并且仅仅被使用在通道操作上。 Select 语句被用来执行多个通道操作的一个和其附带的 case 块代码。
6.7.1 原理
让我们来看下面的例子,讨论下其执行原理
package main
import("fmt""time")var start time.Time
funcinit(){
start = time.Now()}funcservice1(c chanstring){
time.Sleep(3* time.Second)
c <-"Hello from service 1"}funcservice2(c chanstring){
time.Sleep(5* time.Second)
c <-"Hello from service 2"}funcmain(){
fmt.Println("main start", time.Since(start))
chan1 :=make(chanstring)
chan2 :=make(chanstring)goservice1(chan1)goservice2(chan2)select{case res :=<-chan1:
fmt.Println("Response form service 1", res, time.Since(start))case res :=<-chan2:
fmt.Println("Response form service 2", res, time.Since(start))}
fmt.Println("main stop ",time.Since(start))}/*
结果:
main start 0s
Response form service 1 Hello from service 1 3.0018445s
main stop 3.0019815s
*/
从上面的程序来看,我们知道 select 语句和 switch 很像,不同点是用通道读写操作代替了布尔操作。通道将被阻塞,除非它有默认的 default 块 (之后将介绍)。一旦某个 case 条件执行,它将不阻塞。
所以一个 case 条件什么时候执行呢 ?
如果所有的 case 语句(通道操作)被阻塞,那么 select 语句将阻塞直到这些 case 条件的一个不阻塞(通道操作),case 块执行。 如果有多个 case 块(通道操作)都没有阻塞,那么运行时将随机选择一个不阻塞的 case 块立即执行。
为了演示上面的程序,我们开启两个协程并传入对应的通道变量。然后我们写一个带有两个 case 操作的 select 语句。 一个 case 操作从 chan1 读数据,另外一个从 chan2 读数据。这两个通道都是无缓冲的 , 读操作将被阻塞 。所以 select 语句将阻塞。因此 select 将等待,直到有 case 语句不阻塞。
package main
import("fmt""time")var start time.Time
funcinit(){
start = time.Now()}funcservice1(c chanstring){
c <-"Hello from service 1"}funcservice2(c chanstring){
c <-"Hello from service 2"}funcmain(){
fmt.Println("main start", time.Since(start))
chan1 :=make(chanstring)
chan2 :=make(chanstring)goservice1(chan1)goservice2(chan2)select{case res :=<-chan1:
fmt.Println("Response form service 1", res, time.Since(start))case res :=<-chan2:
fmt.Println("Response form service 2", res, time.Since(start))}
fmt.Println("main stop ",time.Since(start))}
结果一: main start 0s Response form service 1 Hello from service 1 539.3µs main stop 539.3µs 结果二: main start 0s Response form service 2 Hello from service 2 0s main stop 0s
结果一共有2!个不同的结果
为了证明当所有 case 块都是非阻塞的时候,golang 会随机选择一个代码块执行打印 response,我们使用缓冲通道来改造程序。
package main
import("fmt""time")var start time.Time
funcinit(){
start = time.Now()}funcservice1(c chanstring){
c <-"Hello from service 1"}funcservice2(c chanstring){
c <-"Hello from service 2"}funcmain(){
fmt.Println("main start", time.Since(start))
chan1 :=make(chanstring,2)
chan2 :=make(chanstring,2)
chan1 <-"Value 1"
chan1 <-"Value 2"
chan2 <-"Value 1"
chan2 <-"Value 2"select{case res :=<-chan1:
fmt.Println("Response form service 1", res, time.Since(start))case res :=<-chan2:
fmt.Println("Response form service 2", res, time.Since(start))}
fmt.Println("main stop ",time.Since(start))}
上述的程序的结果是有不同的
结果一: main start 0s Response form service 1 Value 1 496.2µs main stop 496.2µs 结果二: main start 0s Response form service 2 Value 1 0s main stop 0s
在上面的程序中,两个通道在其缓冲区中都有两个值。因为我们向容量为 2 的缓冲区通道分别发送了两个值,所以这些通道发送操作不会阻塞并且会执行下面的 select 块。 select 块中的所有 case 操作都不会阻塞,因为每个通道中都有两个值,而我们的 case 操作只需要取出其中一个值。因此,go 运行时会随机选择一个 case 操作并执行其中的代码。
6.7.2 default case 块
像 switch 一样, select 语句也有 default case 块。default case 块 是非阻塞的,不仅如此, default case 块可以使 select 语句永不阻塞,这意味着, 任何通道的 发送 和 接收 操作 (不管是缓冲或者非缓冲) 都不会阻塞当前线程。
package main
import("fmt""time")
var start time.Time
func init(){
start = time.Now()}
func service1(c chan string){
c <- "Hello from service 1"}
func service2(c chan string){
c <- "Hello from service 2"}
func main(){
fmt.Println("main start", time.Since(start))
chan1 := make(chan string)
chan2 := make(chan string)
go service1(chan1)
go service2(chan2)select{case res :=<-chan1:
fmt.Println("Response form service 1", res, time.Since(start))case res :=<-chan2:
fmt.Println("Response form service 2", res, time.Since(start))
default:
fmt.Println("No Response received",time.Since(start))}
fmt.Println("main stop ",time.Since(start))}
/*
结果:
main start 0s
No Response received 0s
main stop 0s
*/
package main
import("fmt""time")var start time.Time
funcinit(){
start = time.Now()}funcservice1(c chanstring){
fmt.Println("service1 start")
c <-"Hello from service 1"}funcservice2(c chanstring){
fmt.Println("service2 start")
c <-"Hello from service 2"}funcmain(){
fmt.Println("main start", time.Since(start))
chan1 :=make(chanstring)
chan2 :=make(chanstring)goservice1(chan1)goservice2(chan2)
time.Sleep(3*time.Second)select{case res :=<-chan1:
fmt.Println("Response form service 1", res, time.Since(start))case res :=<-chan2:
fmt.Println("Response form service 2", res, time.Since(start))default:
fmt.Println("No Response received",time.Since(start))}
fmt.Println("main stop ",time.Since(start))}/*
结果不唯一。
main start 0s
service2 start
service1 start
Response form service 1 Hello from service 1 3.0006729s
main stop 3.0006729s
*/
6.7.3 空 select
和 for{} 这样的空循环很像,空 select{} 语法也是有效的。但是有一点必须要说明。 我们知道 select 将被阻塞除非有 case 块没有阻塞。因为 select{} 没有 case 非阻塞语句,主线程将阻塞并可能会导致死锁。
package main
import"fmt"funcservice(){
fmt.Println("Hello from service")}funcmain(){
fmt.Println("main started")goservice()select{}
fmt.Println("main stop")}/*
结果
main started
Hello from service
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
*/
在上面的程序中我们知道 select 将阻塞 main 线程,调度器将会调度 service 这个协程。在 service 执行完毕后,调度器会再去调度其他可用的协程,但是此时已经没有可用的协程,主线程也正在阻塞,所以最后的结果就是发生死锁.
package main
import"fmt"funcsquare(c chanint){
fmt.Println("[square] reading")
num :=<-c
c <- num * num
}funccube(c chanint){
fmt.Println("[cube] reading")
num :=<-c
c <- num * num * num
}funcmain(){
fmt.Println("[main] main started")
squareChan :=make(chanint)
cubeChan :=make(chanint)gosquare(squareChan)gocube(cubeChan)
testNum :=3
fmt.Println("[main] send testNum to squareChan")
squareChan <- testNum
fmt.Println("[main] resuming")
fmt.Println("[main] send testNum to cubeChane")
cubeChan <- testNum
fmt.Println("[main] resuming")
fmt.Println("[main] reading from channels")
squareVal,cubeVal :=<-squareChan,<-cubeChan
sum := squareVal + cubeVal
fmt.Println("[main] sum of square and cube of",testNum," is",sum)
fmt.Println("[main] main stop")}/*
结果:
[main] main started
[main] send testNum to squareChan
[cube] reading
[square] reading
[main] resuming
[main] send testNum to cubeChane
[main] resuming
[main] reading from channels
[main] sum of square and cube of 3 is 36
[main] main stop
*/
流程:
创建两个函数 square 和 cube 作为协程运行。
两个函数都有一个 int 类型通道参数c,从 c 中读取数据到变量num,最后把计算的数据再写入到通道 c 中。
在主线程中使用 make函数创建两个 int类型通道 squareChan and cubeChan
package main
import("fmt""sync""time")funcservice(wg *sync.WaitGroup, instance int){
time.Sleep(2* time.Second)
fmt.Println("Service called on instance",instance)
wg.Done()//协程数-1}funcmain(){
fmt.Println("main started")var wg sync.WaitGroup
for i:=1;i<=3; i++{
wg.Add(1)goservice(&wg,i)}
wg.Wait()//阻塞
fmt.Println("main stop")}/*
结果:(结果是不唯一的,一共有3!次可能的结果)
main started
Service called on instance 2
Service called on instance 1
Service called on instance 3
main stop
*/
当前我们有 3 个work 协程在工作,我们使用了 sleep 操作来模拟阻塞操作,所以调度器在某一个阻塞的时候会去调用其他的 work 协程,当某个 work 协程 sleep 完成后会把计算数字的平方的结果数据放入 results 缓冲无阻塞通道。
当 3 个协程依次交替把 task 通道的任务都完成后,for range 循环将完成,并且因为之前我们已经关闭了任务通道,所以协程也不会发生死锁。调度器将继续返回调度主线程。
有时候所有的工作协程可能都在阻塞,此时调度器将去调度主线程,直到 results 通道再次为空。 当所有 work 协程都完成任务退出后,主线程将继续拿到调度权并打印 results 通道剩下的数据,继续之后代码的执行。
9. Mutex
互斥是 Go 中一个简单的概念。在我解释它之前,先要明白什么是竞态条件。 goroutines 都有自己的独立的调用栈,因此他们之间不分享任何数据。但是有一种情况是数据存放在堆上,并且被多个 goroutines 使用。 多个 goroutines 试图去操作一个内存区域的数据会造成意想不到的后果.
package main
import("fmt""sync")var i intfuncworker(wg *sync.WaitGroup){
i = i+1
wg.Done()}funcmain(){
fmt.Println("main started")var wg sync.WaitGroup
for i:=0;i<1000;i++{
wg.Add(1)goworker(&wg)}
wg.Wait()
fmt.Println("main stop",i)}/*
结果是不同的!!
main started
main stop 985
*/
i = i + 1 这个计算有 3 步 (1) 得到 i 的值 (2) 给 i 的值加 1 (3) 更新 i 的值
在 Go 中,互斥数据结构 ( map) 由 sync 包提供。在 Go 中,多协程去操作一个值都可能会引起竞态条件。我们需要在操作数据之前使用 mutex.Lock() 去锁定它,一旦我们完成操作,比如上面提到的 i = i + 1, 我们就可以使用 mutext.Unlock() 方法解锁。
如果在锁定的时候,有一个协程想要读写 i 的值,那么此协程将阻塞 直到前面的协程完成操作并解锁数据。因此在某一时刻有且仅有一个协程可以操作数据,从而避免竞态条件。记住,任何锁之间的变量在解锁之前对于其他协程都不是可用的。
让我们使用互斥锁修改上面的例子
package main
import("fmt""sync")var i intfuncworker(wg *sync.WaitGroup,m *sync.Mutex){
m.Lock()
i = i+1
m.Unlock()
wg.Done()}funcmain(){
fmt.Println("main started")var wg sync.WaitGroup
var m sync.Mutex
for i:=0;i<1000;i++{
wg.Add(1)goworker(&wg,&m)}
wg.Wait()
fmt.Println("main stop",i)}/*结果
main started
main stop 1000
*/