40、Go语言基础 - 并发编程 - channel(通道)

作者: 温新

分类: 【Go基础】

阅读: 333

时间: 2023-12-05 00:34:25

hi,我是温新

channel 概述

**在 Go 语言中,channel 是协程之间的通信机制。**一个 channel 是一条通信的管道,它可以让一个协程通过 channel 给另一个协程发送数据。每个 channel 都需要指定数据类型,即 channel 可发送数据的类型。如果使用 channel 发送 int 类型数据,可以写成 chan int。数据发送的方式如同水在管道中的流动。

在 Go 语言中,提倡使用 channel 的方式替代共享内存来进行协程间的数据共享。换而言之,Go 主张通过数据传递来实现共享内存,而不是通过共享内存来实现数据传递。

Go 语言中的**通道(channel)是一种特殊的类型。**通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

创建 channel 类型

声明通道

声明 channel 类型的语法格式如下:

var 通道变量 chan 元素类型

通道变量:保存通道的变量;

元素类型:通道内的数据类型。

chan类型的空值是 nil,声明后需要配合 make() 才能使用。

案例:

package main

import "fmt"

func main() {
	// 声明通道
	var ch chan int
	fmt.Println(ch) // nil
	// 使用 make 初始化
	ch = make(chan int, 1)
	fmt.Println(ch) // 0xc000070120
}

创建通道

通道是引用类型,需要使用 make 进行创建,格式如下:

通道实例 := make(chan 数据类型, [缓冲区大小])

通道实例:通过 make 创建的通道句柄;

数据类型:通道内传输的元素类型。

案例:

package main

import "fmt"

func main() {
	ch := make(chan int)
	fmt.Println(ch) // 0xc000070120
}

操作 channel

通道创建后,可以使用通道进行发送和接收操作。

通道发送数据

通道发送使用特殊的操作符<-,将数据通过通道发送的格式如下:

# 含义:把 值 赋值给 通道变量

通道变量 <- 值

通道变量:通过 make 创建好的通道实例;

:可以是变量、常量、表达式或函数返回值等。值的类型必须与 ch 通道的元素类型一致。

使用通关接收数据

通道接收数据同样使用 <- 符号,通道接收有如下特性:

  • 1、通道的收发操作在不同的两个 goruntine 中进行;
    • 由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另一个 goruntine 中进行。
  • 2、接收将持续阻塞知道发送方发送数据;
    • 若接收方接收时,通道中没有发送方发送的数据,接收方也会发生阻塞,知道发送方发送数据为止。
  • 3、每次接收一个元素。
    • 通道一次只能接收一个数据元素。

通道的数据接收有如下 4 种方式:

方式 1:阻塞接收数据

data := <- ch

# 含义:从通道 ch 中取出数据并赋值给 data

该方式接收会阻塞,直到接收到数据并赋值给 data 变量。

方式 2:非阻塞接收数据

data, ok := <- ch

data:表示收到的数据。未接收到数据时,data 为通道类型的 零值。

ok:表示是否接收到数据。

非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。若需要实现接收超时检测,可以配合 select 和 计时器channel 进行。

方式 3:接收任意数据,忽略接收的数据

阻塞接收数据后,忽略从通道返回的数据,其格式如下:

<- ch

该方式将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。其目的不在于接收 channel 中的数据,而是为了阻塞 goruntine。

方式 4:循环接收

使用 for range 循环接收,格式如下:

for data := range ch {
}

通道 ch 是可以遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。

使用 channel 时需要考虑发生死锁(deadlock)的可能。如果 goruntine 在一个 channel 上发送数据,其他的 channel 应用接收得到的数据;如果没有接收,那么程序将运行时出现死锁。如果 goruntine 正在等待从 channel 接收数据,其他一些 goruntine 将会在该 channel 上写入数据;若没有写入,程序将会死锁。

接收数据的案例

基本案例
package main

import "fmt"

func main() {
	ch := make(chan int, 1)
	// 向 ch 通道中发送数据
	ch <- 10
	// 从通道中接收数据
	data := <-ch
	fmt.Println(data)
	// 关闭通道
	close(ch)
}
方式 1:阻塞接收
package main

import "fmt"

func main() {
	ch1 := make(chan string)
	go sendData(ch1)

	// 接收数据:方式 1
	for {
		data := <-ch1
        
        // 通道关闭,通道中传输的数据则为和数据类型的默认值
        // chan int 默认值为 0,
        // chan string 默认值 空
        
        // 等于空,数据传输完毕,中断循环执行
		if data == "" {
			break
		}
		fmt.Println("从通道中读取数据的方式 1:", data)
	}
    // 方式 2
    // 方式 3
    // 方式 4
}

func sendData(ch1 chan string) {
	// 关闭通道
	defer close(ch1)

	for i := 0; i < 3; i++ {
		ch1 <- fmt.Sprintf("发送数据%d\n", i)
	}
	fmt.Println("数据发送完毕")
}

输出结果

从通道中读取数据的方式 1: 发送数据0
从通道中读取数据的方式 1: 发送数据1
从通道中读取数据的方式 1: 发送数据2

在这个案例中,sendData 函数中发送发 3 次数据;而通道一次只能取出一条数据,因此使用循环来读取全部数据。

下面的方式2、3、4 都将在 main 函数中执行,因此不贴出所有代码了。

**方式 2:非阻塞接收 **
for {
    data, ok := <-ch1
    fmt.Println(ok)
    // 通过返回值形式来判断通道是否关闭
    // 若通道关闭,则 ok 值返回 false
    
    
    // 通道关闭,则中断循环
    if !ok {
        break
    }
    fmt.Println("从通道中读取数据的方式 2:", data)
}

输出结果

true
从通道中读取数据的方式 2: 发送数据1

true
从通道中读取数据的方式 2: 发送数据2

数据发送完毕
false
方式 3:for range 接收
for data := range ch1 {
    fmt.Println("从通道中读取数据的方式 4:", data)
}

缓冲 channel

默认创建的都是非缓冲 channel,读写都是即时阻塞。

缓冲 channel 自带一块缓冲区,可以暂时存储数据,若缓冲区满了,就会发生阻塞。

无缓冲的 channel

无缓冲的通道又称为阻塞的通道。

package main

import "fmt"

func main() {
	ch := make(chan string)
	ch <- "王美丽" // 这里形成死锁
	fmt.Println("send succ")
}

该代码可以通过编译,但是会在运行时发生错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()

deadlock 即死锁。表示这个程序中的 goruntine 被挂起导致程序死锁。出现死锁的原因如下:

使用 ch := make(chan string) 创建的是无缓冲的通道。无缓冲的 channel 只有在接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。

解决之道,创建一个 goruntine 去接收通道的数据,如下:

package main

import (
	"fmt"
)

func main() {
	ch := make(chan string)
	// 创建一个通道接收数据
	go recv(ch)
	// 向通道中写入数据
	ch <- "王美丽"
	fmt.Println("send succ")
}

func recv(ch chan string) {
	// 接收通道中的数据
	res := <-ch
	fmt.Println("数据接收成功,名字是:", res)
}

有缓冲的 channel

解决死锁的另一种方法是使用缓冲区,可以在使用 make 时进行初始化。

package main

import "fmt"

func main() {
	ch := make(chan string, 1)
	ch <- "王美丽"

	fmt.Println("发送成功")
}

单向 channel

channel 默认都是双向的,即可读可写。定向 channel 也叫单向 channel,只读或只写。

只读 channel 使用格式如下:

make(<- chan Type)
<- chan

只写 channel 使用格式如下:

make(chan <- Type)
chan <- data

案例

package main

import (
	"fmt"
	"time"
)

func main() {
	// 双向 channel
	ch1 := make(chan string)
	go func1(ch1)
	data := <-ch1
	fmt.Println("main 函数接收到的数据:", data)

	ch1 <- "王美丽在学习 Go"
	ch1 <- "hao"
	go func2(ch1)
	go func3(ch1)
	time.Sleep(1 * time.Second)
	fmt.Println("succ")

}

func func1(ch1 chan string) {
	ch1 <- "我是王美丽"
	data := <-ch1
	data2 := <-ch1
	fmt.Println("回应:", data, data2)
}

// 只写入数据
func func2(ch1 chan<- string) {
	// 只能写入
	ch1 <- "你好鸭,王美丽"
}

// 只读数据
func func3(ch1 <-chan string) {
	data := <-ch1
	fmt.Println("只读:", data)
	//ch1 <- "hello" // Invalid operation: ch1 <- "hello" (send to the receive-only type <-chan string)
}

输出结果:

main 函数接收到的数据: 我是王美丽
回应: 王美丽在学习 Go hao
只读: 你好鸭,王美丽
succ
操作/状态 nil 无值 有值
发送 nil 发送成功 发送成功 阻塞
接收 阻塞 阻塞 接收成功 接收成功
关闭 panic 关闭成功 关闭成功 关闭成功

select 多路复用

有些时候需要从多个 channel 中接收数据,channel 在接收数据时,若没有数据可以被接收,那么当前 goruntine 将会发生阻塞。Go 中内置了关键字 select 用于响应多个 channel 的操作。

select 类似于 switch,也有 case 分支和默认分支。每个 case 分支对应一个 channel 的通信过程。select 会一直等待,知道其中的某个 case 的通信操作完成时,就会执行 case 分支对应的代码,其使用格式如下:

select {
case <-ch1:
    //
case data := <-ch2:
	//
case ch3 <- 10:
	//
default:
    // 默认操作
}

select 语句的特点如下:

  • 1、可以处理一个或多个 channel 的发送/接收操作;
  • 2、若有多个 case 同时满足,select 会随机选择一个执行;
  • 3、对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。
package main

func main() {
	ch := make(chan int, 1)
	go func(chan int) {
		for {
			select {
			// 0或者1写入是随机的
			case ch <- 0:
			case ch <- 1:
			}
		}
	}(ch)
    
	for i := 0; i < 10; i++ {
		println(<-ch)
	}
}
请登录后再评论