28《Go语言入门》无缓冲通道

本文深入探讨Go语言中无缓冲通道的使用方法与注意事项,通过四个实例展示其工作原理及常见应用场景,包括同步机制、模拟网球比赛以及多协程间的通信。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

通道的种类

通道分为无缓冲通道有缓冲通道
实际上,选择有缓冲通道或无缓冲通道将改变应用程序的行为和性能。

区别就是创建的时候是否分配大小

  • 无缓存channel
    • var ch1 = make(chan type),未分配大小
    • var ch2 = make(chan type, 0),分配大小为0也等同给于未分配大小
  • 有缓存channel
    • ch := make(chan type, size),size为分配的大小

无缓冲通道

在前面的文章中,讲的都是无缓冲通道。

无缓冲通道指的是在接收前没有能力保存任何值的通道。
该通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。
下图展示了两个goroutine如何利用无缓冲通道来共享一个值:
使用无缓冲通道在goroutine之间同步

  1. 第1步两个goroutine都到达通道
  2. 第2部左侧的goroutine将它的手伸进通道
    这是向通道发送数据的行为。该goroutine会在通道中被锁住。
  3. 第3步右侧的goroutine将它的手伸进通道
    这是从通道接收数据的行为。该goroutine会在通道中被锁住。
  4. 第4步和第5步进行交换
  5. 第6步两个goroutine将它们的手从通道拿出来
    两个goroutine从锁住变为释放

示例1

下面的代码能很好说明无缓冲通道的使用:

func TestUnbufferedChan1(t *testing.T) {
	c := make(chan string)

	// 等待一组线程的结束
	var wg sync.WaitGroup
	// 设定等待完成的协程数
	wg.Add(2)

	go func() {
		// 当前协程执行完成,就是把需要等待的协程数减1
		defer wg.Done()
		t.Log("下面的代码(发送者)会阻塞")
		c <- `foo`
		t.Log("发送者停止阻塞")
	}()

	go func() {
		// 当前协程执行完成,就是把需要等待的协程数减1
		defer wg.Done()

		time.Sleep(time.Second * 2)
		t.Log(`Message: ` + <-c)
		t.Log("接收者已经收到了")
	}()

	t.Log("等待协程执行完成")
	// 等待协程执行完成
	wg.Wait()

	t.Log("完成")
}

WaitGroup在24 - 并发编程goroutine这篇文章中有讲到过

示例2

在网球比赛中,两位选手会把球在两个人之间来回传递。选手总是处在以下两种状态之一,要么在等待接球,要么将球打向对方。可以使用两个 goroutine 来模拟网球比赛,并使用无缓冲的通道来模拟球的来回,代码如下所示:

// wg用来等待程序结束
var wg sync.WaitGroup

func TestUnbufferedChan(t *testing.T) {
	// 创建一个无缓冲通道
	court := make(chan int)

	// 计数加2,表示要等待两个goroutine
	wg.Add(2)

	// 启动两个选手
	go player("A", court)
	go player("B", court)

	// 发球
	court <- 1

	// 灯带游戏结束
	wg.Wait()
}

func player(name string, court chan int) {
	defer wg.Done()

	fmt.Println(name)

	for {
		// 等待球被击打过来
		ball, ok := <-court
		if !ok {
			// 如果通道被关闭,我们就赢了
			fmt.Printf("Player %s Won\n", name)
			return
		}

		// 这是由于rand是一种伪随机数,你可以通过设定不同seed(种子)的方法来解决这个问题
		rand.Seed(time.Now().UnixNano())
		// 选随机数,然后用这个数来判断我们是否丢球
		n := rand.Intn(100)
		if n%13 == 0 {
			fmt.Printf("Player %s Missed\n", name)
			// 关闭通道,表示我们输了
			close(court)
			return
		}

		// 显示击球数,并将击球数加1
		fmt.Printf("Player %s Hit %d\n", name, ball)
		ball++

		// 将球打向对手
		court <- ball
	}
}

输出结果你自己猜猜先。

示例3

3个协程,一个用于不停打印生成数字,一个负责对生成的数字计算平分,另一个负责打印计算的结果。
分析:只需要两个通道即可,一个用于传数字,一个用传结果。

func TestUnbufferedChan2(t *testing.T) {
	naturals := make(chan int)
	squares := make(chan int)
	// 阻塞主协程
	block := make(chan int)

	// counter
	go func() {
		for i := 0; ; i++ {
			// 限制下循环速度
			time.Sleep(2 * time.Second)

			t.Log("counter", "准备发送i")
			naturals <- i
			t.Log("counter", "已发送i")
		}
	}()

	// squarer
	go func() {
		for {
			t.Log("squarer", "准备接收naturals")
			i := <- naturals
			t.Log("squarer", "已接收naturals")

			t.Log("squarer", "准备发送squares")
			squares <- i * i
			t.Log("squarer", "已发送squares")
		}
	}()

	// printer
	go func() {
		for {
			t.Log("printer", "准备接收squares")
			t.Log(<-squares)
			t.Log("printer", "已接收squares")
		}
	}()

	block <- 0
}

示例4

示例3是无限计算,如果我想有限计算3个呢?
这还不简单,直接将无限循环改为有限循环3次即可啊!

	// counter
	go func() {
		for i := 0; i < 3; i++ {
			// 限制下循环速度
			time.Sleep(2 * time.Second)

			t.Log("counter", "准备发送i")
			naturals <- i
			t.Log("counter", "已发送i")
		}
	}()

😯 但是计算3次后竟然报错了:fatal error: all goroutines are asleep - deadlock!
原因很简单:
当循环3次跳出循环后,counter函数结束。而squarer和printer这两个协程由于一直接收不到消息而被阻塞,并且主协程也被block这个通道阻塞。程序中所有goroutine都被阻塞了,并且是永远的阻塞了,所以会报错。如果所有的goroutine是由于调用了sleep暂时被阻塞,则不会。

正确的做法是:循环3次后,关闭通道。使用for range来接收,接收后关闭通道。

如果你对关闭通道的使用还不熟悉,请参考我写的27 - 单向通道(chan)里面有讲解关闭通道

func TestUnbufferedChan4(t *testing.T) {
	naturals := make(chan int)
	squares := make(chan int)
	// 阻塞主协程
	block := make(chan int)

	// counter
	go func() {
		for i := 0; i < 3; i++ {
			// 限制下循环速度
			time.Sleep(2 * time.Second)

			t.Log("counter", "准备发送i")
			naturals <- i
			t.Log("counter", "已发送i")
		}
		close(naturals)
	}()

	// squarer
	go func() {
		// 只有naturals关闭后,for range才会结束
		for i := range naturals {
			t.Log("squarer", "准备发送squares")
			squares <- i * i
			t.Log("squarer", "已发送squares")
		}
		close(squares)
	}()

	// printer
	go func() {
		for res := range squares {
			t.Log("printer", "准备接收squares")
			t.Log(res)
			t.Log("printer", "已接收squares")
		}
		close(block)
	}()

	t.Log("结束", <-block)
}
=== RUN   TestUnbufferedChan4
    28_unbuffered_chan_test.go:196: counter 准备发送i
    28_unbuffered_chan_test.go:198: counter 已发送i
    28_unbuffered_chan_test.go:207: squarer 准备发送squares
    28_unbuffered_chan_test.go:209: squarer 已发送squares
    28_unbuffered_chan_test.go:217: printer 准备接收squares
    28_unbuffered_chan_test.go:218: 0
    28_unbuffered_chan_test.go:219: printer 已接收squares
    28_unbuffered_chan_test.go:196: counter 准备发送i
    28_unbuffered_chan_test.go:198: counter 已发送i
    28_unbuffered_chan_test.go:207: squarer 准备发送squares
    28_unbuffered_chan_test.go:209: squarer 已发送squares
    28_unbuffered_chan_test.go:217: printer 准备接收squares
    28_unbuffered_chan_test.go:218: 1
    28_unbuffered_chan_test.go:219: printer 已接收squares
    28_unbuffered_chan_test.go:196: counter 准备发送i
    28_unbuffered_chan_test.go:198: counter 已发送i
    28_unbuffered_chan_test.go:207: squarer 准备发送squares
    28_unbuffered_chan_test.go:209: squarer 已发送squares
    28_unbuffered_chan_test.go:217: printer 准备接收squares
    28_unbuffered_chan_test.go:218: 4
    28_unbuffered_chan_test.go:219: printer 已接收squares
    28_unbuffered_chan_test.go:224: 结束 0
--- PASS: TestUnbufferedChan4 (6.01s)
PASS

支持🤟


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值