为何使用Semaphore会降低Go网页爬取程序的运行速度?
问题分析与解决方案
性能下降的核心原因
你的爬虫程序瓶颈根本不在CPU,而是网络IO。原程序创建大量goroutine时,这些goroutine大部分时间都在等待网络响应(属于阻塞的IO操作)。Go的调度器会自动把空闲的线程(M)分配给其他可运行的goroutine,所以虽然goroutine数量多,但并不会占满所有CPU核心线程,反而能充分利用等待IO的间隙并发处理更多请求。
而你用semaphore把并发数限制为runtime.GOMAXPROCS(0)(即CPU核心数),相当于人为把并发请求数降到了CPU核心级别——但网络IO的延迟远高于CPU处理时间,这直接导致单位时间内处理的请求数大幅减少,总耗时自然翻倍。
控制goroutine数量同时保性能的方案
不要把并发数绑定到CPU核心数,而是根据网络请求的承载能力设置一个合理的并发值(比如20、50甚至100,具体看目标网站的反爬策略和你的网络带宽)。
推荐用带缓冲的通道实现并发控制,比semaphore更轻量直观:
package main import ( "fmt" "net/http" "sync" ) func main() { // 模拟1000个待爬取URL urls := make([]string, 1000) for i := range urls { urls[i] = fmt.Sprintf("https://example.com/page/%d", i+1) } // 设置合理的并发数,根据实际情况调整 maxConcurrency := 50 sem := make(chan struct{}, maxConcurrency) var wg sync.WaitGroup for _, url := range urls { wg.Add(1) // 占用一个并发名额 sem <- struct{}{} go func(u string) { defer wg.Done() // 释放并发名额 defer func() { <-sem }() // 发起请求并处理 resp, err := http.Get(u) if err != nil { fmt.Printf("请求%s失败: %v\n", u, err) return } defer resp.Body.Close() // 这里添加页面解析、数据提取等逻辑 }(url) } wg.Wait() }
为什么这个方案有效
带缓冲通道的并发控制逻辑中,goroutine在等待网络响应时会被Go调度器挂起,空闲下来的线程会去处理其他就绪的goroutine。所以即使并发数远大于CPU核心数,也不会浪费CPU资源,同时又能控制goroutine的总量(避免无限制创建导致的内存占用过高),最终性能能接近原程序的水平。
内容的提问来源于stack exchange,提问作者akopyl
相关产品推荐
相关产品推荐

