You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 12:05:42