
本文旨在解释在 Golang 并发编程中,为何使用缓冲通道(buffered channel)有时反而比非缓冲通道(unbuffered channel)更慢。我们将通过示例代码分析,深入探讨缓冲通道的初始化开销以及它对程序性能的影响,并提供优化建议。
在 Golang 中,通道(channel)是 goroutine 之间进行通信和同步的重要机制。通道分为缓冲通道和非缓冲通道两种类型。直观上,缓冲通道似乎应该比非缓冲通道更高效,因为它允许发送者在通道未满时继续发送数据,而无需立即等待接收者。然而,在某些情况下,缓冲通道的性能反而不如非缓冲通道。这通常与缓冲通道的初始化开销有关。
让我们通过一个具体的例子来理解这个问题。以下代码片段展示了一个使用缓冲通道和非缓冲通道的 HTML 文本提取程序:
package mainimport ( "fmt" "math/rand" "os" "sync" "time" sel "code.google.com/p/go-html-transform/css/selector" h5 "code.google.com/p/go-html-transform/h5" gnhtml "code.google.com/p/go.net/html")// Find a specific HTML element and return its textual element children.func main() { test := ` This is the test document! header: color=blue; This is some text ` // Get a parse tree for this HTML h5tree, err := h5.NewFromString(test) if err != nil { die(err) } n := h5tree.Top() // Create a Chain object from a CSS selector statement chn, err := sel.Selector("#h") if err != nil { die(err) } // Find the item. Should be a div node with the text "This is some text" h := chn.Find(n)[0] // run our little experiment this many times total var iter int = 100000 // When buffering, how large shall the buffer be? var bufSize uint = 100 // Keep a running total of the number of times we've tried buffered // and unbuffered channels. var bufCount int = 0 var unbufCount int = 0 // Keep a running total of the number of nanoseconds that have gone by. var bufSum int64 = 0 var unbufSum int64 = 0 // Call the function {iter} times, randomly choosing whether to use a // buffered or unbuffered channel. for i := 0; i < iter; i++ { if rand.Float32() < 0.5 { // No buffering unbufCount += 1 startTime := time.Now() getAllText(h, 0) unbufSum += time.Since(startTime).Nanoseconds() } else { // Use buffering bufCount += 1 startTime := time.Now() getAllText(h, bufSize) bufSum += time.Since(startTime).Nanoseconds() } } unbufAvg := unbufSum / int64(unbufCount) bufAvg := bufSum / int64(bufCount) fmt.Printf("Unbuffered average time (ns): %vn", unbufAvg) fmt.Printf("Buffered average time (ns): %vn", bufAvg)}// Kill the program and report the errorfunc die(err error) { fmt.Printf("Terminating: %vn", err.Error()) os.Exit(1)}// Walk through all of a nodes children and construct a string consisting// of c.Data where c.Type == TextNodefunc getAllText(n *gnhtml.Node, bufSize uint) string { var texts chan string if bufSize == 0 { // unbuffered, synchronous texts = make(chan string) } else { // buffered, asynchronous texts = make(chan string, bufSize) } wg := sync.WaitGroup{} // Go walk through all n's child nodes, sending only textual data // over the texts channel. wg.Add(1) nTree := h5.NewTree(n) go func() { nTree.Walk(func(c *gnhtml.Node) { if c.Type == gnhtml.TextNode { texts <- c.Data } }) close(texts) wg.Done() }() // As text data comes in over the texts channel, build up finalString wg.Add(1) finalString := "" go func() { for t := range texts { finalString += t } wg.Done() }() // Return finalString once both of the goroutines have finished. wg.Wait() return finalString}
在这个例子中,getAllText 函数使用 goroutine 和 channel 来提取 HTML 节点中的文本。它接受一个 bufSize 参数,用于指定通道的缓冲区大小。如果 bufSize 为 0,则使用非缓冲通道;否则,使用具有指定缓冲区大小的缓冲通道。
立即学习“go语言免费学习笔记(深入)”;
最初的实验结果表明,使用缓冲区大小为 100 的缓冲通道的平均运行时间明显高于非缓冲通道。这似乎违反了直觉。
原因分析
关键在于缓冲通道的初始化开销。当创建一个缓冲通道时,Go 运行时需要分配一块内存来存储通道中的元素。缓冲区越大,分配的内存就越多。在上述例子中,每次调用 getAllText 函数时,都会创建一个新的缓冲通道。如果缓冲区大小设置得过大,频繁的内存分配和回收可能会导致性能下降。
优化方案
为了验证这个假设,我们将缓冲区大小从 100 减小到 10。再次运行程序,得到的结果如下:
Buffered average time (ns): 21930Buffered average time (ns): 22721Buffered average time (ns): 23011Buffered average time (ns): 23707Buffered average time (ns): 27701Buffered average time (ns): 28325Buffered average time (ns): 28851Buffered average time (ns): 29641Buffered average time (ns): 30417Buffered average time (ns): 32600Unbuffered average time (ns): 21077Unbuffered average time (ns): 21490Unbuffered average time (ns): 22332Unbuffered average time (ns): 22584Unbuffered average time (ns): 26438Unbuffered average time (ns): 26824Unbuffered average time (ns): 27322Unbuffered average time (ns): 27926Unbuffered average time (ns): 27985Unbuffered average time (ns): 30322
可以看到,使用缓冲区大小为 10 的缓冲通道的平均运行时间与非缓冲通道的平均运行时间非常接近。这表明,减小缓冲区大小可以有效地降低初始化开销,从而提高程序性能。
总结与建议
在 Golang 并发编程中,缓冲通道并非总是比非缓冲通道更高效。缓冲通道的初始化开销可能会对程序性能产生负面影响,尤其是在频繁创建和销毁通道的情况下。选择合适的缓冲区大小非常重要。过大的缓冲区可能会导致内存分配和回收的开销增加,而过小的缓冲区则可能导致通道阻塞。在实际应用中,应根据具体情况选择合适的通道类型和缓冲区大小。如果通道的创建和销毁频率很高,并且传输的数据量较小,则可以考虑使用非缓冲通道或较小的缓冲通道。
此外,还可以考虑以下优化策略:
重用通道: 避免频繁创建和销毁通道。如果可能,可以重用已有的通道,以减少内存分配和回收的开销。使用 sync.Pool: 可以使用 sync.Pool 来管理缓冲通道,从而避免频繁的内存分配。分析程序瓶颈: 使用性能分析工具(如 pprof)来识别程序中的性能瓶颈,并针对性地进行优化。
通过深入理解缓冲通道的特性和潜在的性能问题,我们可以编写出更高效、更可靠的 Golang 并发程序。
以上就是Golang并发:缓冲通道为何有时比非缓冲通道慢?的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1412375.html
微信扫一扫
支付宝扫一扫