go语言学习笔记 — 进阶 — 并发编程(6):通道(channel)—— 使用channel收发数据

创建channel之后,我们可以使用channel进行数据收发操作。

使用channel发送(填充)数据

使用特殊的操作符<-,把数据通过channel发送。

  • 格式
通道变量 <- 值

通道变量是上文通过make创建的通道实例;值可以是变量、常量、表达式或函数返回值,值类型必须与make(chan 数据类型)中的数据类型一致。

  • 发送数据

在使用make创建channel后,就可以使用<-向通道发送数据。

ch := make(chan interface{}) // 创建一个空接口通道

ch <- 0        // 把整型数据放入通道
ch <- "hello"  // 把字符串放入通道
  • 若数据未被接收,则发送数据将持续阻塞,直到数据被接收

把数据向通道发送,如果接收方一致没有接收,那么发送操作将持续阻塞,直到数据被接收方接收。

package main

func main() {
    ch := make(chan int)
    ch <- 0
}

// fatal error: all goroutines are asleep - deadlock!

go会智能发现一些永远无法发送成功的语句,并提示。上面的意思是,go运行时发现所有goroutine(包括main()函数的)都处于waiting状态,即所有goroutine并没有发送或接收数据对应的通道。


使用channel接收(取出)数据

通道接收(取出)数据的特性

  • 通道收发数据需要在两个不同的goroutine之间进行

通过channel发送数据,如果通道中没有另外一个goroutine接收,数据发送方goroutine会持续阻塞,直到接收方goroutine接收数据;通过channel接收数据,如果通道中没有发送方goroutine发送数据,数据接收方goroutine将持续阻塞,直到发送方goroutine发送数据为止。

  • 通道一次只能接收一个元素数据

通道接收(取出)数据的格式

一共有4种通道接收数据的写法。

  • 阻塞模式接收数据

把接收变量作为<-操作符的左值。执行此语句,发送方将会持续阻塞,直至接收方接收数据并赋值给data变量。

data := <-ch
  • 非阻塞模式接收数据(高CPU占用,少用)

使用非阻塞方式从通道接收数据,语句不会阻塞。

data, ok := <-ch

data表示接收数据的通道变量,在没有接收到数据时,data为通道类型的零值。ok表示是否成功从通道接收到数据。

  • 接收任意数据,忽略接收的数据

阻塞模式接收数据后,忽略从通道返回的数据。

<-ch

执行此语句,将会发生数据接收阻塞,直至接收到数据,但接收到的数据会被忽略。此方式使用channel在不同goroutine间阻塞收发,实现并发同步。

package main

import "fmt"

func main() {
    ch := make(chan int)  // 构建一个同步通道
    
    go func() {           // 开一个匿名函数的并发
        fmt.Println("start goroutine")
        ch <- 0           // 匿名函数快结束时,通过channel告知main()函数的goroutine,数据发送会一直阻塞,直到main()函数的goroutine接收为止。 
        fmt.Println("exit goroutine")
    }()

    fmt.Println("wait goroutine")
    
    <-ch  // 开启匿名函数的goroutine后,阻塞模式等待发送数据,接收任意数据后忽略
    
    fmt.Println("all done")
}

/*
wait goroutine
start goroutine
exit goroutine
all done
*/
  • 循环接收

通道数据可以使用for range语句,进行多个元素的接收。遍历得到变量data。格式如下:

for data := range ch {
    循环体
}

通道是可遍历的,遍历结果是接收到的数据,其数据类型是通道的元素类型。

package main

func main() {
    ch := make(chan int)  // 生成一个整型元素的通道
    
    go func() {           // 把匿名函数并发执行
        for i := 3;i >= 0; i-- {
            ch <- i       // 使用通道发送值i
            time.Sleep(time.Second) // 每次发送完暂停1秒
        }
    }()
   
   for data := range ch {  // 使用for range从通道中接收数据
       fmt.Println(data)   // 打印接收的变量
       
       if data == 0 {      // 当接收到0时,终止for range
           break           // 如果发送端继续发,接收goroutine已经推出,会报panic
       }
   }
}

/*
3
2
1
0
*/

使用goroutine和channel,实现并发打印

设计生产者与消费者模型:生产者是main函数的for循环,消费者是printer函数。整个模型使用两个goroutine:一个是main(),另一个是go printer创建的goroutine,两个goroutine通过第21行的通道进行通信。

这个通道有两个功能:

  • 数据传送:第26行发送数据,第9行接收数据
  • 控制指令:类似信号量的功能,同步goroutine操作。如第17行、29行、31行
package main

import "fmt"

// 定义打印函数
func printer(c chan int) {
    
    for {              // 启动一个死循环
        data := <-c    // 从函数入参channel中取得整型数据
        if data == 0 { 
            break
        }
    
        fmt.Println(data) // 输出刚取得的整型数据   
    }
    
    c <- 0  // 退出循环时,通知main函数(我搞定了)
}

func main() {
    c := make(chan int)  // 创建一个channel
    
    go printer(c)  // 并发执行,传入channel
    
    for i := 1; i <= 10; i++ { // 使用for循环,把1~10通过通道c发送给printer函数的goroutine
        c <- i
    }
    
    c <- 0  // 发完数据后,通知并发的printer结束循环(没数据了)
    
    <-c     // 阻塞式等待接收printer发送退出数据,然后main函数才能结束(搞定喊我),
} 
上一篇:讲讲进程,线程以及goroutine


下一篇:go gmp --- goroutine创建源码分析