
本文深入探讨go语言中缓冲与非缓冲通道的性能差异,特别是在特定并发求和场景下的表现。我们将分析为何在接收方即时可用的情况下,非缓冲通道与缓冲通道的性能可能趋同,以及缓冲机制何时才能真正发挥其解耦与提升吞吐量的优势。通过代码示例和理论分析,旨在帮助开发者更准确地理解go通道的同步特性与性能边界。
在Go语言中,通道(Channel)是并发编程的核心原语,用于goroutine之间的通信。通道分为两种主要类型:
非缓冲通道 (Unbuffered Channel): 通过make(chan T)创建。非缓冲通道的发送操作会阻塞,直到有接收者准备好接收数据;同样,接收操作也会阻塞,直到有发送者发送数据。这是一种严格的同步机制,也被称为“会合(rendezvous)”通信,确保发送和接收同时发生。
缓冲通道 (Buffered Channel): 通过make(chan T, capacity)创建,其中capacity指定了通道可以存储的元素数量。缓冲通道的发送操作只有在缓冲区满时才会阻塞;接收操作只有在缓冲区空时才会阻塞。它允许发送者和接收者在一定程度上解耦,无需立即同步。
通常,我们期望缓冲通道由于其异步特性(在缓冲区未满时发送不会阻塞)能够提供更好的性能,尤其是在生产者-消费者模式中,当生产速度和消费速度不匹配时,缓冲通道可以平滑数据流,提高整体吞吐量。
考虑一个典型的并发求和场景:将一个大型数组分成若干子段,每个子段由一个独立的goroutine计算局部和,然后将局部和发送到一个主goroutine进行最终汇总。这种模式通常采用“扇出(fan-out)”和“扇入(fan-in)”的结构。
以下是一个简化的代码结构示例,展示了如何使用通道进行并发求和:
package main
import (
"fmt"
"math/rand"
"runtime"
"sync"
"time"
)
// generateRandomNumbers 生成随机数数组
func generateRandomNumbers(size int) []int {
nums := make([]int, size)
for i := 0; i < size; i++ {
nums[i] = rand.Intn(100)
}
return nums
}
// linearSum 线性求和
func linearSum(nums []int) int {
sum := 0
for _, n := range nums {
sum += n
}
return sum
}
// worker 计算局部和并发送到通道
func worker(nums []int, ch chan int, wg *sync.WaitGroup) {
defer wg.Done()
localSum := 0
for _, n := range nums {
localSum += n
}
ch <- localSum
}
// channelSum 使用非缓冲通道进行并发求和
func channelSum(nums []int, numWorkers int) int {
ch := make(chan int) // 非缓冲通道
var wg sync.WaitGroup
chunkSize := len(nums) / numWorkers
for i := 0; i < numWorkers; i++ {
start := i * chunkSize
end := (i + 1) * chunkSize
if i == numWorkers-1 {
end = len(nums) // 确保最后一个worker处理剩余部分
}
wg.Add(1)
go worker(nums[start:end], ch, &wg)
}
// 启动一个goroutine等待所有worker完成并关闭通道
go func() {
wg.Wait()
close(ch)
}()
totalSum := 0
for s := range ch { // 主goroutine接收局部和
totalSum += s
}
return totalSum
}
// bufferedChannelSum 使用缓冲通道进行并发求和
func bufferedChannelSum(nums []int, numWorkers int, bufferSize int) int {
ch := make(chan int, bufferSize) // 缓冲通道
var wg sync.WaitGroup
chunkSize := len(nums) / numWorkers
for i := 0; i < numWorkers; i++ {
start := i * chunkSize
end := (i + 1) * chunkSize
if i == numWorkers-1 {
end = len(nums)
}
wg.Add(1)
go worker(nums[start:end], ch, &wg)
}
go func() {
wg.Wait()
close(ch)
}()
totalSum := 0
for s := range ch {
totalSum += s
}
return totalSum
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) // 设置GOMAXPROCS为CPU核心数
arraySize := 100000000 // 1亿个数字
numWorkers := runtime.NumCPU() // 使用CPU核心数作为worker数量
nums := generateRandomNumbers(arraySize)
// 线性求和
start := time.Now()
sumLinear := linearSum(nums)
fmt.Printf("线性求和: %d, 耗时: %v\n", sumLinear, time.Since(start))
// 非缓冲通道求和
start = time.Now()
sumCh := channelSum(nums, numWorkers)
fmt.Printf("非缓冲通道求和: %d, 耗时: %v\n", sumCh, time.Since(start))
// 缓冲通道求和 (缓冲区大小为numWorkers)
start = time.Now()
sumBufCh := bufferedChannelSum(nums, numWorkers, numWorkers)
fmt.Printf("缓冲通道求和 (buffer=%d): %d, 耗时: %v\n", numBufCh, time.Since(start))
// 缓
冲通道求和 (缓冲区大小为1)
start = time.Now()
sumBufCh1 := bufferedChannelSum(nums, numWorkers, 1)
fmt.Printf("缓冲通道求和 (buffer=1): %d, 耗时: %v\n", sumBufCh1, time.Since(start))
}在这个场景中,多个worker goroutine计算完局部和后,会尝试将结果发送到通道。同时,主goroutine(或另一个聚合goroutine)会通过for s := range ch循环不断地从通道接收这些局部和。
根据Go通道的内部机制,当一个非缓冲通道的发送操作发生时,如果此时已经有一个接收操作在等待,那么数据会直接从发送者传递给接收者,这个过程是原子且高效的,几乎没有额外的等待时间。这种直接的“会合”通信意味着,发送者不需要等待缓冲区写入,接收者也不需要等待缓冲区读取,双方直接交换数据。
在上述并发求和的场景中,主goroutine会持续地从通道中读取数据。这意味着,当一个worker goroutine计算完局部和并尝试发送时,很可能主goroutine已经准备好接收了。在这种情况下:
因此,在这类“即时接收”的通信模式下,非缓冲通道与缓冲通道的性能差异变得微乎其微。两者都能够以接近直接数据传递的效率完成通信,因为“等待同步”的时间被最小化了。缓冲通道的优势在于能够解耦发送者和接收者,允许它们以不同的节奏运行,而当它们节奏同步且接收者总是准备就绪时,这种解耦的价值就不那么明显了。
千鹿Pr助手
智能Pr插件,融入众多AI功能和海量素材
128
查看详情
在实际的基准测试中,我们可能会观察到:
这种现象的原因在于:
因此,在接收方总是准备好接收数据的场景下,缓冲通道并不能带来显著的性能提升,因为非缓冲通道的同步成本也极低。
虽然在特定场景下缓冲通道的性能优势不明显,但在许多其他并发模式中,缓冲通道依然是不可或缺的:
在Go语言中选择使用缓冲通道还是非缓冲通道,不应仅仅基于对“异步更快”的直觉,而应深入理解其背后的同步机制和通信模式。
对于基准测试,有几点需要注意:
最终,选择哪种通道类型,应该根据具体的应用场景、通信需求和对同步/解耦的权衡来决定。在接收方即时可用的简单扇入扇出模式中,非缓冲通道与缓冲通道的性能差异往往可以忽略不计。
以上就是Go语言中缓冲与非缓冲通道的性能考量:深入理解同步与异步通信的详细内容,更多请关注其它相关文章!
相关文章:
J*a中实现Go语言select通道多路复用机制
12306选座怎么选到商务座_12306商务座选择与配置说明
Lar*el 递归关系中排除指定分支的教程
AI泡沫首次被“刺破”:GPU十年都无法存活!
c++ 获取系统当前时间 c++时间戳获取方法
Adobe PDF表单中利用J*aScript解析与格式化日期组件的教程
AO3官方可用镜像 Archive of Our Own网页版最新入口
age动漫网站入口 age动漫官网直接访问入口
Django通过AJAX异步上传图片并保存至模型的完整指南
Win11怎么安装Linux子系统 Win11 WSL2安装Ubuntu及环境配置指南
J*aScript map 方法中处理循环元素为空数组的策略
怎样使用“本地安全策略”提升Windows安全性_Secpol.msc配置指南【高手】
优化HTML表单样式:解决输入框焦点跳动与元素间距问题
虫虫漫画精品漫画官网_虫虫漫画精品漫画官网进入精品漫画
Lar*el头像管理:图片缩放与旧文件删除的最佳实践
Golang如何使用bytes.Split分割字节切片_Golang bytes切片分割方法
Win11 USB传输速度慢怎么解决 Win11 USB驱动更新与设置
包子漫画官方网站阅读入口-包子漫画在线漫画官网直达链接
Go语言中构建可靠数据存储的原子性与持久化策略
Python类型检查:优化关联可选属性的Mypy推断策略
Angular中父组件异步更新子组件复选框状态的实践指南
sublime侧边栏怎么增强功能_SideBarEnhancements for sublime安装与配置
纯CSS与HTML网格布局的HTML精简策略:SVG与JS方案解析
绝地鸭卫平a核爆刀流玩法攻略
QQ邮箱官方网站登录入口_QQ邮箱网页版在线使用
sublime怎么进行远程开发编辑_配置rsub/rmate实现sublime编辑服务器文件
在J*a中如何开发在线活动报名与管理系统_活动报名管理项目实战解析
J*aScript动态修改指定div内所有a标签样式指南
Node.js中HTML按钮与J*aScript函数交互的正确姿势
印象笔记怎样用批量导出备知识库_印象笔记用批量导出备知识库【备份方法】
Python:递归比较文件夹内容并找出特定类型文件的差异
AO3镜像入口大全 AO3网页版内容访问全集
汽水音乐在线版入口_汽水音乐网页播放手册
千牛数据看板网页版_千牛数据看板网页版访问方法
React列表渲染与独立状态管理:避免全局状态影响局部更新
HTML元素状态管理:根据DIV内容动态启用/禁用按钮
J*a实现学校排课程序_面向对象结构化项目示例
c++如何使用Meson构建系统_c++比CMake更快的构建工具
拼多多购物车商品数量无法修改如何处理 拼多多购物车操作优化方法
Composer中的^和~符号代表什么_精通Composer版本号语义化约束
Highcharts 雷达图径向轴标签定制指南:利用多Y轴实现数值标注
将HTML动态表格多行数据保存到Google Sheet的教程
如何创建没有密码的Windows本地账户_跳过微软账户登录的技巧【教程】
Shopware订单中获取产品自定义字段的实用指南
在J*a中如何在J*a中使用异常机制记录错误日志_异常日志实践经验
谷歌邮箱注册显示错误Gmail服务器异常与延迟处理
qq音乐在线播放入口_qq音乐电脑版登录链接
126邮箱账号注册 电脑版登录入口
PPT平滑切换怎么做 PPT炫酷“平滑”切换动画制作教程【必学】
Win11怎么开启省电模式_Win11电池节电模式自动开启