Go 通道(channel)与协程间通信

协程间通信

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

在 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 <- int1 表示:将变量 int1 放入通道中,用通道 ch 发送变量 int1
  • 接收(数据从通道中流出)
    int2 = <- ch 表示:变量 int2 从通道 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)
}

输出结果:

在 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)
}

输出结果:

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

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

而上面这段代码中的 sendData() 方法阻塞了 main() 方法,导致 go getData()无法执行,也就是说通道的接收操作也就无法被执行,而 sendData() 中的发送操作也会一直等待,这就导致程序无法继续运行。这就是死锁(deadlock)形式。

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

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

   go sendData(ch)
   getData(ch)

   time.Sleep(1e9)
}

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

猜你喜欢

转载自www.cnblogs.com/liyutian/p/10203261.html