本文内容纲要:

- 协程间通信

- 声明与初始化

- 通信操作符

- 通道阻塞

- 带缓冲的通道

协程间通信

协程中可以使用共享变量来通信,但是很不提倡这样做,因为这种方式给所有的共享内存的多线程都带来了困难。

在 Go 中有一种特殊的类型,通道(channel),就像一个可以用于发送类型化数据的管道,由其负责协程之间的通信,从而避开所有由共享内存导致的陷阱;这种通过通道进行通信的方式保证了同步性。

数据在通道中进行传递:在任何给定时间,一个数据被设计为只有一个协程可以对其访问,所以不会发生数据竞争。数据的所有权(可以读写数据的能力)也因此被传递。

通道服务于通信的两个目的:值的交换,同步的,保证了两个计算(协程)任何时候都是可知状态。

声明与初始化

通道的声明格式如下:

var identifier chan datatype

未初始化的通道的值为 nil。
从声明的格式能够看出来,通道只能传输一种类型的数据,比如 chan int 或者 chan string,所有的类型都可以用于通道,空接口 interface{} 也可以。

通道也是引用类型,所以我们使用 make() 函数来给它分配内存。

var ch1 chan string
ch1 = make(chan string)
//或者简写为
ch2 := make(chan string)

通信操作符

<-
ch <- int1int2 = <- ch

下面的例子展示了两个协程之间的通信:

import (
   "fmt"
   "time"
)

func main() {
   ch := make(chan string)

   go sendData(ch)
   go getData(ch)

   time.Sleep(1e9)
}

func sendData(ch chan string){
  ch <- "golang"
}

func getData(ch chan string){
  fmt.Println(<- ch)
}

输出结果:

Image

在 main() 方法的最后一行中,使用了 time 包中的 sleep 函数来暂停1秒,以确保 main() 方法会在另个两个协程之后结束,如果不在 main() 方法中等待,协程会随着程序的结束而消亡。

通道阻塞

默认情况下,通信是同步且无缓冲的,通道的发送/接收操作在对方准备好之前都是阻塞的:

  • 对于同一个通道,在没有接受者接收数据之前,发送操作会被阻塞。
  • 对于同一个通道,在没有发送者发送数据之前,接收操作会被阻塞。

现在我们把上面的例子修改一下,去掉 sendData() 方法前的 go 关键字:

func main() {
    ch := make(chan string)
	
    //go sendData(ch)
	sendData(ch)
    go getData(ch)
	
    time.Sleep(1e9)
}

输出结果:

Image

运行程序后出错了,抛出了一个 panic,这是为什么呢?

这是因为 Go 程序在运行时会检查所有的协程,查找是否存在有阻塞(读取或者写入某个通道)的情况。

go getData()

如果我们接着再修改一下代码,保留 sendData() 方法的关键字,而去掉 getData() 方法的关键字:

func main() {
  ch := make(chan string)

   go sendData(ch)
   getData(ch)

   time.Sleep(1e9)
}

因为发送和接收操作都会被执行,所以结果是正常输出“golang”。

通信是一种同步形式:通过通道,两个协程在通信(协程会和)中某刻同步交换数据。无缓冲通道成为了多个协程同步的完美工具。

带缓冲的通道

一个无缓冲通道只能包含 1 个元素,但是我们可以为通道提供了一个缓存,来扩展通道可容纳的元素个数。

在 make() 函数中设置容量:

buf := 100
ch1 := make(chan string, buf)
  • 在缓冲被满载(缓冲被全部使用)之前,给一个带缓冲的通道发送数据是不会阻塞的
  • 在缓冲被清空之前,从通道读取数据也不会阻塞。

如果容量大于 0,通道就是异步的了:缓冲满载(发送)或者变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收。如果容量是 0 或者未设置,通信仅在收发双方准备好的情况下才可以成功。

本文内容总结:协程间通信,声明与初始化,通信操作符,通道阻塞,带缓冲的通道,