26《Go语言入门》并发编程chan — goroutine之间通信的管道

这是我纯手写的《Go语言入门》,手把手教你入门Go。源码+文章,看了你就会🥴!
文章中所有的代码我都放到了github.com/GanZhiXiong/go_learning这个仓库中!
看文章时,对照仓库中代码学习效果更佳哦!

前言

并发编程的难度在于协调,而协调就要通过交流,从这个角度看来,并发单元间的通信是最大的问题。

常用的并发通信模型有两种:共享数据和消息机制

共享数据

是指多个并发单元分别通过内存数据块或磁盘文件或网络数据等(实际应用中共享内存用得最多)保持对同一个数据的引用,实现对该数据的共享。

消息通信机制

共享数据的方式在并发时,为了防止数据被意外改变而导致访问的数据不准确,在操作时需要给数据加锁,操作完成后,再将锁打开。

想象一下,在一个大的系统中具有无数的锁、无数的共享变量、无数的业务逻辑与错误处理分支,那将是一场噩梦。这噩梦就是众多 C/C++ 开发者正在经历的,其实 Java 和 C# 开发者也好不到哪里去。

而Go语言提供的是另一种通信模型,即以消息机制而非共享内存作为通信方式。

消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量,但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。
这有点类似于进程的概念,每个进程不会被其他进程打扰,它只做好自己的工作就可以了。不同进程间靠消息来通信,它们不会共享内存。

Go语言提供的消息通信机制被称为 channel

chan

Go语言提倡使用消息通信机制的方法代替共享内存,当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。

这里的通信方法就是使用通道(channel)即chan:

在这里插入图片描述

chan的特性

Go语言中的通道(channel)是一种特殊的类型。在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。

通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。

声明chan

通道本身需要一个类型进行修饰,就像切片类型需要标识元素类型。通道的元素类型就是在其内部传输的数据类型,声明如下:

var 通道变量 chan 通道类型

chan类型的空值为nil,是没有分配空间的,声明后需要配合make后才能使用。

创建chan

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

通道实例 := make(chan 数据类型)

如:

ch1 := make(chan int)
ch2 := make(chan interface{})
type a struct {}
ch3 := make(chan *a) 

使用chan发送数据

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

  • 通道发送数据的格式
通道变量 <-

注意发送值可以使变量、常量、表达式或函数返回值等,但值的类型必须与通道的元素的类型一致。

如:

func TestChan1(t *testing.T) {
	ch := make(chan interface{})
	ch <- 0
	ch <- "hello"
}

什么情况,上面的测试用例运行后却报错了:

=== RUN   TestChan1
fatal error: all goroutines are asleep - deadlock!

原因是把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。Go程序运行时能智能地发现一些永远无法发送成功的语句并作出提示。

使用chan接收数据

通道接收同样使用<-操作符。

chan接收的特性

  • 通道的收发操作在不同的两个goroutine间进行
  • 发送方将持续阻塞直到接收方接收数据
  • 接收方将持续阻塞直到发送方发送数据
  • 通道一次只能接收一个数据元素

chan接收的写法

一共四种写法。

1、阻塞接收数据

data := <-ch

执行该语句时将会阻塞,直到接收到数据并赋值给data变量。

2、非阻塞接收数据

data, ok := <-ch
  • data为接收到的数据,未接收到数据时,data为通道类型的零值。
  • ok为是否接收到数据。

执行该语句时不会阻塞。

该方式可能会造成高的CPU占用,因此很少使用。

如果是需要实现接收超时检测,可以配合select和计时器channel进行,后面我会讲到的。

3、阻塞接收任意数据,忽略接收的数据

<-ch

该方式实际上只是通过通道在goroutine间阻塞收发实现并发同步。
如:

// 使用通道做并发同步
func TestChan2(t *testing.T) {
	// 构建一个通道
	ch := make(chan int)
	// 开启一个并发匿名函数
	go func() {
		t.Log("start goroutine")

		// 通过通道通知TestChan2的goroutine
		ch <- 0

		t.Log("exit goroutine")
	}()

	t.Log("wait goroutine")

	// 等待匿名goroutine
	<-ch

	t.Log("all done")
}

4、循环接收

通道的数据接收可以借用for range语句进行多个元素的接收操作:

for data := range ch {

}

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

如:

// 通道接收数据方式4、循环接收
func TestChan3(t *testing.T) {
	// 构建一个通道
	ch := make(chan int)

	// 开启一个并发匿名函数
	go func() {
		for i := 3; i >= 0; i-- {
			t.Log("go func for", i)
			// 发送3到0之间的数值
			ch <- i

			// 每次发送完成时等待1s
			time.Sleep(time.Second)
		}
	}()
	
	// 遍历接收通道数据
	for data := range ch {
		t.Log(data)

		if data == 0 {
			break
		}
	}
}
=== RUN   TestChan3
    25_chan_test.go:57: go func for 3
    25_chan_test.go:68: 3
    25_chan_test.go:57: go func for 2
    25_chan_test.go:68: 2
    25_chan_test.go:57: go func for 1
    25_chan_test.go:68: 1
    25_chan_test.go:57: go func for 0
    25_chan_test.go:68: 0
--- PASS: TestChan3 (3.00s)
PASS

测试题

请先自己猜测结果。
改代码也位于github.com/GanZhiXiong/go_learning这个仓库中!

测试1

// 通道接收数据方式4、循环接收
func TestChan4(t *testing.T) {
	// 构建一个通道
	ch := make(chan int)

	// 开启一个并发匿名函数
	go func() {
		for i := 3; i >= 0; i-- {
			t.Log("go func for", i)
			// 发送3到0之间的数值
			ch <- i

			// 每次发送完成时等待1s
			time.Sleep(time.Second)
		}
	}()

	data := <-ch
	t.Log(data)
}

测试2

// 测试题目2
func TestChanTest2(t *testing.T) {
	// 构建一个通道
	ch := make(chan int)

	// 开启一个并发匿名函数
	go func() {
		for i := 3; i >= 0; i-- {
			t.Log("go func for", i)
			// 发送3到0之间的数值
			ch <- i

			// 每次发送完成时等待1s
			time.Sleep(time.Second)
		}
	}()

	data := <-ch
	t.Log(data)

	time.Sleep(time.Second*4)
}

支持🤟


  • 🎸 [关注❤️我吧],我会持续更新的。
  • 🎸 [点个👍赞吧],码字不易麻烦了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值