通道 | 青训营

67 阅读4分钟

相比Erlang,Go并未实现严格的并发安全。

允许全局变量、指针、引用类型这些非安全内存共享操作,就需要开发人员自行维护数据一致和完整性。Go鼓励使用CSP通道,以通信来代替内存共享,实现并发安全。

Don't communicate by sharing memory,share memory by communicating.

CSP:Communicating Sequential Process.

通过消息来避免竞态的模型除了CSP,还有Actor。但两者有较大区别。

作为CSP核心,通道(channel)是显式的,要求操作双方必须知道数据类型和具体通道,并不关心另一端操作者身份和数量。可如果另一端未准备妥当,或消息未能及时处理时,会阻塞当前端。

相比起来,Actor是透明的,它不在乎数据类型及通道,只要知道接收者信箱即可。默认就是异步方式,发送方对消息是否被接收和处理并不关心。

从底层实现上来说,通道只是一个队列。同步模式下,发送和接收双方配对,然后直接复制数据给对方。如配对失败,则置入等待队列,直到另一方出现后才被唤醒。异步模式抢夺的则是数据缓冲槽。发送方要求有空槽可供写入,而接收方则要求有缓冲数据可读。需求不符时,同样加入等待队列,直到有另一方写入数据或腾出空槽后被唤醒。

除传递消息(数据)外,通道还常被用作事件通知。

func main() { 
   done:=make(chan struct{})     // 结束事件 
   c:=make(chan string)            // 数据传输通道 
  
   go func() { 
       s:= <-c                    // 接收消息 
       println(s)          
       close(done)                     // 关闭通道,作为结束通知 
    }() 
  
   c<- "hi!"                    // 发送消息 
    <-done                         // 阻塞,直到有数据或管道关闭 
}

输出:

hi!

同步模式必须有配对操作的goroutine出现,否则会一直阻塞。而异步模式在缓冲区未满或数据未读完前,不会阻塞。

func main() { 
   c:=make(chan int,3)            // 创建带3个缓冲槽的异步通道 
  
   c<-1                 // 缓冲区未满,不会阻塞 
   c<-2
  
   println(<-c)             // 缓冲区尚有数据,不会阻塞 
   println(<-c) 
}

输出:

1
2

多数时候,异步通道有助于提升性能,减少排队阻塞。

缓冲区大小仅是内部属性,不属于类型组成部分。另外通道变量本身就是指针,可用相等操作符判断是否为同一对象或nil。

func main() { 
   var a,b chan int=make(chan int,3),make(chan int) 
   var c chan bool
  
   println(a==b) 
   println(c==nil) 
  
   fmt.Printf("%p, %d\n",a,unsafe.Sizeof(a)) 
}

输出:

false
true
0xc820076000,8

虽然可传递指针来避免数据复制,但须额外注意数据并发安全。

内置函数cap和len返回缓冲区大小和当前已缓冲数量;而对于同步通道则都返回0,据此可判断通道是同步还是异步。

func main() { 
   a,b:=make(chan int),make(chan int,3) 
  
   b<-1
   b<-2
  
   println("a:",len(a),cap(a)) 
   println("b:",len(b),cap(b)) 
}

输出:

a:0 0
b:2 3

收发

除使用简单的发送和接收操作符外,还可用ok-idom或range模式处理数据。

func main() { 
   done:=make(chan struct{}) 
   c:=make(chan int) 
  
   go func() { 
       defer close(done)          // 确保发出结束通知 
  
       for{ 
           x,ok:= <-c
           if!ok{                     // 据此判断通道是否被关闭 
               return
            } 
  
           println(x) 
        } 
    }() 
  
   c<-1
   c<-2
   c<-3
   close(c)    
  
    <-done
}

输出:

1
2
3

对于循环接收数据,range模式更简洁一些。

func main() { 
   done:=make(chan struct{}) 
   c:=make(chan int) 
  
   go func() { 
       defer close(done) 
  
       for x:=range c{         // 循环获取消息,直到通道被关闭 
           println(x) 
        } 
   }() 
  
   c<-1
   c<-2
   c<-3
   close(c) 
  
    <-done
}

及时用close函数关闭通道引发结束通知,否则可能会导致死锁。

fatal error:all goroutines are asleep-deadlock!

通知可以是群体性的。也未必就是通知结束,可以是任何需要表达的事件。

func main() { 
   var wg sync.WaitGroup
   ready:=make(chan struct{}) 
  
   for i:=0;i<3;i++ { 
       wg.Add(1) 
  
       go func(id int) { 
           defer wg.Done() 
  
           println(id, ":ready.")       // 运动员准备就绪 
            <-ready                       // 等待发令 
           println(id, ":running...") 
        }(i) 
    } 
  
   time.Sleep(time.Second) 
   println("Ready?Go!") 
  
   close(ready)                 // 砰! 
  
   wg.Wait() 
}

输出:

0:ready. 
2:ready. 
1:ready. 
  
Ready?Go! 
  
1:running... 
0:running... 
2:running...

一次性事件用close效率更好,没有多余开销。连续或多样性事件,可传递不同数据标志实现。还可使用sync.Cond实现单播或广播事件。

对于closed或nil通道,发送和接收操作都有相应规则:

  • 向已关闭通道发送数据,引发panic。
  • 从已关闭接收数据,返回已缓冲数据或零值。
  • 无论收发,nil通道都会阻塞。
func main() { 
   c:=make(chan int,3) 
  
   c<-10
   c<-20
   close(c) 
  
   for i:=0;i<cap(c)+1;i++ { 
       x,ok:= <-c
       println(i, ":",ok,x) 
    } 
}

输出:

0:true 10
1:true 20
2:false 0
3:false 0

重复关闭,或关闭nil通道都会引发panic错误。

panic:close of closed channel
panic:close of nil channel