专业编程基础技术教程

网站首页 > 基础教程 正文

Go语言并发之channel学习笔记

ccvgpt 2024-08-06 12:51:58 基础教程 12 ℃

前言


上一篇我们了解了Go语言的goroutine,本篇来回顾下Go语言中的channel。

Go语言并发之channel学习笔记

channel简介

Channel是Go中的一个核心类型,我们可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication)。

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

channel类型

channel是一种引用类型,声明通道类型的格式如下:

var 变量 chan 元素类型
var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool  // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
chan T          // 可以接收和发送类型为 T 的数据
chan<- float64  // 只可以用来发送 float64 类型的数据
<-chan int      // 只可以用来接收 int 类型的数据

<-代表channel的方向。如果没有指定方向,那么Channel就是双向的,既可以接收数据,也可以发送数据。

创建channel

通道是引用类型,通道类型的空值是nil。声明通道后需要使用make函数初始化之后才能使用。创建channel的格式如下:

make(chan 元素类型, [缓冲大小])

channel的缓冲大小是可选的。

func main()  {
	var ch1 chan int	// 引用类型,需要初始化才能使用
	fmt.Println(ch1)	//<nil>
	ch1 = make(chan int,2)	// 带缓冲区通道,异步通道
	//ch1 = make(chan int)	// 无缓冲区通道,又称为同步通道,必须要同步接受
	ch1 <- 10	// 往通道中发送值
	fmt.Println(ch1)	//0xc0000ba000
	fmt.Println(len(ch1))	//1
	x:= <- ch1	// 从通道中取值
	//len(ch1)  取通道元素数量
	//cap(ch1)	取通道元素容量
	fmt.Println(len(ch1))	//0
	fmt.Println(cap(ch1))	//2
	fmt.Println(x)	// 10
	close(ch1)	//关闭通道
}

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

发送

ch1 <- 10	// 往通道中发送值

接收

x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果

关闭

调用内置的close函数来关闭通道

close(ch)

使用channel时有几个注意点:

  • 向一个nil channel发送消息,会一直阻塞
  • 向一个已经关闭的channel发送消息,会引发运行时恐慌(panic)
  • channel关闭后不可以继续向channel发送消息,但可以继续从channel接收消息
  • 当channel关闭并且缓冲区为空时,继续从从channel接收消息会得到一个对应类型的零值
  • 通道是可以被垃圾回收机制回收的,关闭通道不是必须的
  • 只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道

以上3种操作和3种channel状态可以组合出9种情况:

无缓冲区通道

无缓冲的通道又称为阻塞的通道。创建通道的时候,不指定缓冲大小即为无缓冲通道。

使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。无缓冲的通道必须有接收才能发送。

func recv(ch chan int)  {
	ret := <- ch
	fmt.Println("recv success" ,ret)
}
func main()  {
	ch := make(chan int)
	go recv(ch)	// 启用goroutine从通道接收值,若果不接收,那么main函数就会panic
	ch <- 10
	fmt.Println("send success")
}

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

有缓冲区通道

使用make函数初始化通道的时候为其指定通道的容量即为有缓冲区通道,通道的容量表示通道中能存放元素的数量。

可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量。

func main() {
	ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
	ch <- 10
	fmt.Println("发送成功")
}

从通道中循环取值

使用_,ok判断channel是否关闭

场景:读channel,但不确定channel是否关闭时

原理:读已关闭的channel会得到零值,如果不确定channel,需要使用ok进行检测。

ok的结果和含义:

  • true:读到数据,并且通道没有关闭。
  • false:通道关闭,无数据读到。

用法:

if v, ok := <- ch; ok {
    fmt.Println(v)
}

代码示例:

/*
两个goroutine,两个chann
	生成0-100到ch1
	从ch1中取出数据计算平方,结果发送到ch2
*/

//生成0-100到ch1
func p(ch chan int)  {
	for i:=0;i<100;i++{
		ch <- i
	}
	close(ch)
}

//从ch1中取出数据计算平方,结果发送到ch2
func c(ch1 chan int,ch2 chan int)  {
	for {
		temp, ok:= <- ch1
		if !ok{
			break
		}
		ch2 <- temp * temp
	}
	close(ch2)
}
func main()  {
	ch1 := make(chan int,100)
	ch2 := make(chan int,100)
	go p(ch1)
	go c(ch1,ch2)
	// 循环打印ch2
	for{
		temp ,ok := <- ch2
		if !ok{
			break
		}
		fmt.Println(temp)
	}
}

使用for range读channel

场景:当需要不断从channel读取数据时

原理:使用for-range读取channel,这样既安全又便利,当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值。

用法:

for x := range ch{
    fmt.Println(x)
}

代码示例:

func p(ch chan  int)  {	
	for i:=0;i<100;i++{
		ch <- i
	}
	close(ch)
}

func c(ch1 chan int,ch2 chan int)  {		for ret := range ch1{
		ch2 <- ret * ret
	}
	close(ch2)
}

func main()  {
	ch1 := make(chan int,100)
	ch2 := make(chan int,100)
	go p(ch1)
	go c(ch1,ch2)
	for ret := range ch2{
		fmt.Println(ret)
	}
}

单向通道

我们想要限制通道在函数中只能发送或只能接收的时候可以通过单向通道来实现。

代码示例:

func p(ch chan <- int)  {	//定义单向通道,ch只接受
	for i:=0;i<100;i++{
		ch <- i
	}
	close(ch)
}

func c(ch1 <- chan int,ch2 chan <- int)  {	//定义单向通道,ch1只取,ch2只接收
	for ret := range ch1{
		ch2 <- ret * ret
	}
	close(ch2)
}

func main()  {
	ch1 := make(chan int,100)
	ch2 := make(chan int,100)
	go p(ch1)
	go c(ch1,ch2)
	for ret := range ch2{
		fmt.Println(ret)
	}
}

chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;

<-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。

在函数传参及任何赋值操作中可以将双向通道转换为单向通道,但反过来是不可以的。

select多路复用

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。

select可以同时监控多个通道的情况,只处理未阻塞的case。当通道为nil时,对应的case永远为阻塞,无论读写。特殊关注:普通情况下,对nil的通道写操作是要panic的。若只有一个case满足,则只会执行这个case,若多个case满足,则会随机执行。

使用方法:

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

代码示例:

func main()  {
	ch := make(chan int,1)
	for i:=0;i<10;i++{
		select {
		case x:= <- ch:		// i=0,ch中没有值,此时取值为nil,走下面的case,i=1,此时通道有有值为0,打印0,不走下面case,i=2,通道中没有值,取值为nil。走下一个case。所以打印结果为0 2 4 6 8
			fmt.Println(x)
			case ch <- i:	// i=0,将放入到ch中
		default:
			fmt.Println("default")
		}
	}
}

再看一段代码:

func main() {
	tick := time.Tick(time.Second)
	after := time.After(2 * time.Second)
	channel := make(chan int, 1)
	go func() {
		channel <- 1
		close(channel)
	}()
	for {
		select {
		case <-tick:
			fmt.Println("tick 1 second")
		case <-after:
			fmt.Println("after 2 second")
			return
		case value, ok := <- channel:
			if ok {
				fmt.Println("got", value)
			} else {
				fmt.Println("channel is closed")
				time.Sleep(time.Second)
			}
		default:
			fmt.Println("come into default")
			time.Sleep(100 * time.Millisecond)
		}
	}
}

可以通过 value, ok := <- channel 这种形式来判断通道是否退出,ok获取的就是用来判断channel是否关闭的,ok为 true,表示channel正常,否则,channel就是关闭的。time.Tick是go的time包提供的一个定时器的一个函数,它返回一个channel,并在指定时间间隔内,向channel发送一条数据,time.Tick(time.Second)就是每秒钟向这个channel发送一个数据

time.After是go的time包提供的一个定时器的一个函数,它返回一个channel,并在指定时间间隔后,向channel发送一条数据,time.After(2 * time.Second)就是2s后向这个channel发送一个数据.

以上程序会在2s后自动退出。

使用select语句能提高代码的可读性:

  • 可处理一个或多个channel的发送/接收操作。
  • 如果多个case同时满足,select会随机选择一个。
  • 对于没有case的select{}会一直等待,可用于阻塞main函数。

后记


此篇为GO语言并发系列的第二篇,下一篇我们来共同学习下锁的操作。若有不对的地方,欢迎大家拍砖,共同学习。

Tags:

最近发表
标签列表