Go语言中Mutex为何比Channel性能低?爬虫场景实测解惑
问题描述
我正在开发一款网站爬虫程序,用于返回网站状态。分别采用Mutex和Channel两种方案实现,以避免并发写入map时的数据竞争。但基准测试显示,基于Channel的实现性能远优于Mutex版本,想了解其中原因,是否是我对Mutex的使用方式有误?
基准测试结果

实现代码
package concurrency import "sync" type WebsiteChecker func(string) bool type result struct { string bool } func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) var wg sync.WaitGroup var mu sync.Mutex for _, url := range urls { wg.Add(1) go func(u string) { defer wg.Done() mu.Lock() results[u] = wc(u) mu.Unlock() }(url) } wg.Wait() return results } func CheckWebsitesChannel(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) resultChannel := make(chan result) for _, url := range urls { go func(u string) { resultChannel <- result{u, wc(u)} }(url) } for i := 0; i < len(urls); i++ { r := <-resultChannel results[r.string] = r.bool } return results }
测试代码
package concurrency import ( "reflect" "testing" "time" ) func mockWebsiteChecker(url string) bool { time.Sleep(20 * time.Millisecond) if url == "https://localhost:3000" { return false } return true } func TestCheckWebsites(t *testing.T) { websites := []string{ "https://google.com", "https://localhost:3000", "https://blog.gypsydave5.com", } want := map[string]bool{ "https://google.com": true, "https://blog.gypsydave5.com": true, "https://localhost:3000": false, } got := CheckWebsites(mockWebsiteChecker, websites) if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) } } func BenchmarkCheckWebsites(b *testing.B) { urls := make([]string, 1000) for i := 0; i < len(urls); i++ { urls[i] = "a url" } b.ResetTimer() for i := 0; i < b.N; i++ { CheckWebsites(mockWebsiteChecker, urls) } } func BenchmarkCheckWebsitesChannel(b *testing.B) { urls := make([]string, 1000) for i := 0; i < len(urls); i++ { urls[i] = "a url" } b.ResetTimer() for i := 0; i < b.N; i++ { CheckWebsitesChannel(mockWebsiteChecker, urls) } }
问题分析与解决
你的Mutex版本性能差的核心原因是锁的范围过大:
- 目前代码把耗时的
wc(u)(模拟的20ms网站检查操作)放在了mu.Lock()和mu.Unlock()之间,这意味着每个goroutine执行网站检查时都持有锁,其他goroutine必须等待锁释放才能执行任务。原本应该并发的网站检查变成了串行执行,完全丧失了并发优势,性能自然暴跌。 - 而Channel版本中,每个goroutine先独立执行
wc(u)(完全并发,无锁竞争),仅把结果发送到channel;主goroutine从channel接收结果后统一写入map,写入map的操作串行但耗时极短,因此整体性能接近理想并发状态。
修正后的Mutex版本代码
只需要将锁的范围缩小到仅保护map的写入操作,把耗时的网站检查放在锁外:
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) var wg sync.WaitGroup var mu sync.Mutex for _, url := range urls { wg.Add(1) go func(u string) { defer wg.Done() // 先执行耗时的网站检查,无锁 res := wc(u) // 仅在写入map时加锁 mu.Lock() results[u] = res mu.Unlock() }(url) } wg.Wait() return results }
修正后,网站检查操作可以并发执行,锁仅用于保护极短的map写入步骤,性能会和Channel版本基本持平。
内容的提问来源于stack exchange,提问作者Halil İbrahim Yıldırım
相关产品推荐
相关产品推荐

