【Go】C++ 转 Go 第(五)天:Goroutine 与 Channel | Go 并发编程基础

本专栏文章持续更新,新增内容使用蓝色表示。

食用指南

本文适合 C++ 基础 的朋友,想要快速上手 Go 语言。可直接复制代码在 IDE 中查看,代码中包含详细注释和注意事项。

Go 的环境搭建请参考以下文章:

【Go】C++ 转 Go 第(一)天:环境搭建 Windows + VSCode 远程连接 Linux -CSDN博客

Go语言的并发模型建立在 Goroutine 和 channel 之上。

1. Goroutine

Goroutine 理论基础,此处留个空,后期补。

Goroutine 是本质上是一种由 Go 运行时(runtime)调度的轻量级线程。与传统线程相比,goroutine 的创建和销毁开销很小,其初始栈大小仅为 2KB,且支持动态扩容。程序可以同时运行多个 goroutine,它们共享相同的地址空间。

特性

  • 创建机制:通过 go 关键字快速创建,支持命名函数与匿名函数两种形式

  • 生命周期:主 goroutine 的退出将导致程序立即终止,所有子 goroutine 会被强制结束

  • 主动控制:通过 runtime.Goexit() 可实现当前 goroutine 的立即终止

  • 资源管理:结合 defer 语句确保 goroutine 退出时的资源清理与状态维护

  • 通信限制:goroutine 的函数返回值无法直接获取,必须依赖 channel 机制进行跨协程数据传递

goroutine.go

Go 复制代码
package main

import (
	"fmt"
	"runtime"
	"time"
)

// 从goroutine
func newTask() {
	for i := 0; ; i++ {
		fmt.Printf("new Goroutine: i= %d \n", i)
		time.Sleep(1 * time.Second)
	}
}

// 主goroutine
// 主退出,从一同退出
func main() {
	// 创建一个go程去执行newTask()调度
	go newTask()

	// 无参匿名函数
	go func() {
		defer fmt.Println("匿名 A defer")

		// 外层想退出,可以直接return

		func() {
			defer fmt.Println("匿名 B defer")

			// 内部这层使用return只会退到外层,使用以下函数可实现退出
			runtime.Goexit()

			fmt.Println("-----B-----")
			time.Sleep(2 * time.Second)
		}() // 如果没有结尾的()相当于只定义了,未调用
		fmt.Println("-----A-----")
		time.Sleep(2 * time.Second)
	}()

	// 有参匿名函数
	go func(a int, b int) bool {
		fmt.Println("a =", a, "b =", b)
		return true // 从协程和主协程是一个异步的操作
		// 如果想要主go程得到从go程的值,需要借助管道机制
	}(11, 12)

	// 开启这部分的死循环,就不会退出了
	for {
		time.Sleep(1 * time.Second)
	}

	// fmt.Println("main Goroutine exit.\n")
}

运行结果:

2. Channel

goroutine 之间的通信通过channel(通道)实现。通道提供了一种安全、同步的方式,用于在goroutine 之间传递数据。使用通道可以避免多个 goroutine 同时访问共享数据而导致竞态条件的问题,因为管道的读写是原子性的。

2.1 无缓冲 Channel 的同步通信

无缓冲 channel 实现了 goroutine 间的同步数据交换机制,确保发送和接收操作的严格时序一致性,为并发程序提供可靠的通信基础。

1)同步通信:发送操作会阻塞,直到有接收方准备好;接收操作会阻塞,直到有数据可接收。

2)数据消费:数据具有一次性消费特性,读取后数据从 channel 中移除。

3)死锁风险:发送和接收次数必须匹配,不匹配会导致 goroutine 永久阻塞。

1_channel.go

Go 复制代码
// channel 管道,两个goroutine之间的通信机制
// 一、无缓冲的channel
package main

import (
	"fmt"
	"time"
)

func main() {
	// 无缓冲没有地方存放数据
	// 1. 定义无缓冲 channel
	c := make(chan int)

	go func() {
		defer fmt.Println("sub goroutine exited.")
		fmt.Println("sub goroutine is running......")
		// 2. 向channel中写入
		c <- 666
		c <- 555
		time.Sleep(1 * time.Second)
		subNum := <-c
		fmt.Println("subNum =", subNum)
	}()

	// 思考问题:
	// 1) main goroutine会不会比sub goroutine先执行导致无法接收到数据,无法打印出num?
	// 答:无缓冲channel的同步特性确保了数据传输的时序:发送者会等待接收者,接收者会等待数据。
	// 2) 管道中的数据读出来之后还会有吗?
	// 答:channel中的数据具有一次性消费特性,读取后即从channel中移除。
	// 读取次数和发送次数不等会导致goroutine永久阻塞,产生deadlock

	// 3. 从channel中读数据
	// <-和c之间不能有空格
	<-c        // 读取并丢弃
	num := <-c // 读取并赋值
	c <- 100
	fmt.Println("num =", num)
	fmt.Println("main goroutine exit.")

}

运行结果:

2.2 带缓冲 Channel 的异步通信

带缓冲 channel 在同步通信基础上引入异步能力,通过固定大小的缓冲区调节生产者和消费者之间的速率差异,实现更灵活的并发控制。

异步通信:缓冲区未满时,发送操作不会阻塞;缓冲区不为空时,接收操作不会阻塞。

阻塞条件:缓冲区满时,发送操作阻塞;缓冲区空时,接收操作阻塞。

可以通过 len() 和 cap() 函数可实时获取缓冲区的当前使用情况和总容量

2_channel.go

Go 复制代码
// 二、带缓冲的channel
package main

import (
	"fmt"
	"time"
)

func main() {
	// channel缓冲区满,写数据阻塞;channel缓冲区空,读数据阻塞
	// 1. 定义有缓冲 channel
	c := make(chan int, 2)
	fmt.Println("初始channel:len(c) =", len(c), "cap(c) =", cap(c))

	go func() {
		defer fmt.Println("sub goroutine exited.")
		fmt.Println("sub goroutine is running......")
		// 2. 向channel中写入
		for i := 0; i < 4; i++ {
			c <- i
			fmt.Println("sub向channel发送:", i, ",len(c) =", len(c), ",cap(c) =", cap(c))
			time.Sleep(50 * time.Millisecond)
		}
	}()
	// time.Sleep(1 * time.Second)
	for i := 0; i < 2; i++ {
		fmt.Println("main 接收到:", <-c, ",len(c) =", len(c), ",cap(c) =", cap(c))
	}
	time.Sleep(1 * time.Second)
	fmt.Println("main goroutine exited.")
}

运行结果:

2.3 Channel 的关闭与遍历

关闭操作:使用 close(c) 关闭 channel,关闭后不能再发送数据。

数据读取:已关闭的 channel 可以继续读取剩余数据,使用 data, ok := <-c 检测 channel 状态

range 遍历:自动检测 channel 关闭,简化数据读取代码。

注意:重复关闭 channel 或向已关闭 channel 发送数据都会引发运行时 panic。

3_closeChannel.go

Go 复制代码
// 使用close关闭channel
// 确定没有数据发送时,使用close关闭
package main

import "fmt"

func main() {
	// 定义一个channel
	c := make(chan int)
	go func() {
		defer fmt.Println("sub goroutine exited.")
		fmt.Println("sub goroutine is running......")
		for i := 0; i < 4; i++ {
			c <- i
		}
		// 关闭channel
		close(c)
		// 如果没有close(c),会发生死锁,可以注释掉尝试一下

		// 如果向已关闭的channel发送数据会发生以下错误
		// panic: send on closed channel
		// c <- 99
	}()

	// for {
	// 	// 这种写法将作用域限制在if中
	// 	// 条件是ok为true
	// 	if data, ok := <-c; ok {
	// 		fmt.Println("接收到data:", data)
	// 	} else {
	// 		break
	// 	}
	// }
	// 以上代码可以优化为以下的写法
	for data := range c {
		fmt.Println("使用range接收到data:", data)
	}
	defer fmt.Println("main goroutine exited.")
}

运行结果:

2.4 Select 多路复用

select 语句允许在多个通道操作中选择一个执行。这种方式可以有效地处理多个通道的并发操作,避免了阻塞 。

多路监控:同时监听多个 channel 的读写状态,任一通道就绪即可触发相应操作。当多个 case 同时就绪时,随机选择一个执行,避免饥饿现象。

注意:所有 case 都未就绪时会阻塞,可以使用 default 实现非阻塞操作。

应用场景:超时控制(time.After创建一个定时器,在超时后执行特定的操作,避免永久阻塞。);多 channel 数据处理;goroutine 退出控制。

4_selectChannel.go

Go 复制代码
// select 可以在同一流程下监控多个channel的读写状态
// 类似C/C++中的select、poll、epoll
package main

import "fmt"

func fibonacli(c, quit chan int) {
	x, y := 1, 1
	for {
		select {
		case c <- x:
			// 如果 c 可写,则满足此 case
			// 什么时候不满足,取决于 sub 读的次数
			x = y
			y = x + y
		case <-quit:
			fmt.Println("fibonacli end.")
			return
		}

	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)

	// sub
	go func() {
		for i := 0; i < 6; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()

	// main
	fibonacli(c, quit) // 引用语义
}

运行结果:


如有问题或建议,欢迎在评论区中留言~

相关推荐
Moshow郑锴4 小时前
Oracle CLOB中包含不可见的控制字符导致golang中json转换失败问题
oracle·golang·json
妮妮喔妮8 小时前
Go的垃圾回收
开发语言·后端·golang
带土111 小时前
vscode json
vscode·json
冷天气会感冒14 小时前
关闭VSCode的推荐插件(Recommended extensions)提示
ide·vscode·编辑器
golang学习记15 小时前
Go slog 日志打印最佳实践指南
开发语言·后端·golang
Y unes18 小时前
《i.MX6ULL LED 驱动实战:内核模块开发与 GPIO 控制》
linux·c语言·驱动开发·vscode·ubuntu·嵌入式
古一木19 小时前
ROS1+Vscode
ide·vscode·编辑器
YONYON-R&D19 小时前
VSCODE 调试C程序时 打印中文
ide·vscode·编辑器
冷天气会感冒19 小时前
关闭VSCode的GitHub Copilot功能
vscode·github·copilot