
在go语言中,当多个goroutine尝试同时从同一个channel接收数据时,其确切的行为并非由语言规范明确定义,而是由go运行时调度器负责处理。这意味着,哪个goroutine会最终接收到值,是不可预测的。初始的观察可能认为“最后一个启动接收的goroutine获得值”,但这并非普遍规律,尤其在goroutine启动时间、工作负载轻重等因素影响下,调度器可能将所有工作分配给第一个可用的goroutine,而不保证公平性。
例如,考虑以下代码片段:
c := make(chan string)
for i := 0; i < 5; i++ {
go func(i int) {
msg := <-c // 接收消息
c <- fmt.Sprintf("%s, hi from %d", msg, i) // 发送消息
}(i)
}
c <- "original" // 发送初始消息
fmt.Println(<-c) // 接收最终消息在这个例子中,一个消息被所有Goroutine依次传递,每个Goroutine在接收到消息后对其进行修改并再次发送回同一个Channel。最终输出会是original, hi from 0, hi from 1, hi from 2, hi from 3, hi from 4(注意:Go版本更新可能导致输出顺序变化,但核心思想是消息在Goroutine间传递)。这表明,Go的Channel能够支持消息在多个并发执行的Goroutine之间进行接力式传递。
为了编写更健壮、更易于理解的并发代码,在使用Channel时应遵循以下几条指导原则:
Go语言的Channel非常灵活,可以构建多种并发模式。以下是两种常见的模式及其实现。
立即学习“go语言免费学习笔记(深入)”;
这种模式适用于多个Goroutine产生数据,并由一个中心Goroutine统一处理的场景。Go Channel天然支持多个写入者向同一个Channel发送数据,Go运行时会自动交错这些消息。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan string) // 创建一个无缓冲字符串Channel
// 启动5个Goroutine作为写入者
for i := 1; i <= 5; i++ {
go func(id int, co chan<- string) { // 使用只发送Channel类型
for j := 1; j <= 5; j++ {
// 每个Goroutine发送5条消息
co <- fmt.Sprintf("hi from %d.%d", id, j)
time.Sleep(time.Millisecond * 10) // 模拟工作
}
}(i, c) // 将Channel作为形式参数传递
}
// 主Goroutine作为唯一的读取者,接收所有25条消息
for i := 1; i <= 25; i++ {
fmt.Println(<-c) // 从Channel接收并打印消息
}
// 注意:此处没有关闭Channel,因为主Goroutine知道要接收的消息总数,
// 并在接收完成后自然退出。如果写入者数量不确定或需要通知读取者结束,则需要关闭Channel。
}在这个例子中,5个Goroutine并发地向同一个Channel c 发送消息,每个Goroutine发送5条。主Goroutine则负责从Channel中读取所有25条消息。你会发现消息的输出顺序是交错的,这正是并发执行的体现。
这种模式适用于一个Goroutine产生数据,并由多个Goroutine共同消费这些数据的场景。Go Channel同样支持一个写入者向多个读取者分发数据,但每个消息只会由一个读取者接收。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
c := make(chan int) // 创建一个无缓冲整数Channel
var w sync.WaitGroup
w.Add(5) // 初始化WaitGroup,等待5个读取者Goroutine完成
// 启动5个Goroutine作为读取者
for i := 1; i <= 5; i++ {
go func(id int, ci <-chan int) { // 使用只接收Channel类型
defer w.Done() // Goroutine结束时通知WaitGroup
j := 1
for v := range ci { // 循环从Channel接收数据,直到Channel关闭
time.Sleep(time.Millisecond * 50) // 模拟处理时间
fmt.Printf("Reader %d.%d got %d\n", id, j, v)
j += 1
}
}(i, c) // 将Channel作为形式参数传递
}
// 主Goroutine作为唯一的写入者,发送25条消息
for i := 1; i <= 25; i++ {
c <- i // 向Channel发送整数
}
close(c) // 发送完毕后关闭Channel,通知所有读取者Channel不再有新数据
w.Wait() // 等待所有读取者Goroutine完成
fmt.Println("All readers finished.")
}在这个例子中,主Goroutine向Channel c 发送25个整数。同时,5个Goroutine作为读取者,竞争从同一个Channel接收这些整数。每个整数只会被一个读取者接收并处理。为了确保主Goroutine不会在所有读取者完成之前退出,我们使用了sync.WaitGroup。当所有数据发送完毕后,通过close(c)关闭Channel,这将通知所有正在for range循环中等待的读取者Goroutine,Channel已关闭且不再有新数据,它们可以优雅地退出。
Go语言的Channel是实现并发通信的强大工具。虽然多个Goroutine同时接收同一Channel的行为由调度器决定,不具确定性,但通过遵循最佳实践和利用Channel的特性,我们可以构建出高效且易于理解的并发模式。关键在于:将Channel作为形式参数传递以提高代码质量,避免同一Goroutine内对Channel的读写冲突以防止死锁,以及将缓冲视为性能优化手段而非并发安全保障。通过多写入者-单读取者和单写入者-多读取者等模式,Go开发者能够灵活应对各种复杂的并发场景。
以上就是Go语言中多Goroutine与Channel的并发模式解析的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号