代码之家  ›  专栏  ›  技术社区  ›  akopyl

为什么使用信号量会减慢我的Go程序

  •  -1
  • akopyl  · 技术社区  · 11 月前

    我制作了一个程序,使用goroutines抓取网站的所有页面:

    func main() {
        start := time.Now()
    
        knownUrls := getKnownURLs(os.Getenv("SITEMAP_URL"))
    
        var wg sync.WaitGroup
        for index, url := range knownUrls {
            wg.Add(1)
    
            fmt.Printf("%d/%d\n", index+1, len(knownUrls))
    
            go func() {
                if err := indexArticleFromURL(url, client); err != nil {
                    log.Fatalf("Error indexing doc: %s", err)
                }
                wg.Done()
            }()
        }
    
        wg.Wait()
    
        elapsed := time.Since(start)
        fmt.Printf("Took %s", elapsed)
    }
    

    这工作得非常快,准确地说,1000页需要5.9秒。但让我感到困扰的是,如果一个网站有数千个页面,它将创建数千个goroutines。

    所以我用一个名为 semaphore 据我所知,它应该将goroutine的数量限制在处理器可以处理的范围内。不应该降低性能,因为上面的程序在物理上已经无法使用超过CPU所能提供的线程。

    func main() {
        start := time.Now()
        ctx := context.Background()
    
        knownUrls := getKnownURLs(os.Getenv("SITEMAP_URL"))
    
        var (
            maxWorkers = runtime.GOMAXPROCS(0)
            sem        = semaphore.NewWeighted(int64(maxWorkers))
        )
    
        for index, url := range knownUrls {
            if err := sem.Acquire(ctx, 1); err != nil {
                log.Printf("Failed to acquire semaphore: %v", err)
                break
            }
    
            fmt.Printf("%d/%d\n", index+1, len(knownUrls))
    
            go func() {
                if err := indexDocFromURL(url, client); err != nil {
                    log.Fatalf("Error indexing doc: %s", err)
                }
                sem.Release(1)
            }()
        }
    
        if err := sem.Acquire(ctx, int64(maxWorkers)); err != nil {
            log.Printf("Failed to acquire semaphore: %v", err)
        }
    
        elapsed := time.Since(start)
        fmt.Printf("Took %s", elapsed)
    }
    

    但现在,当我运行程序时,它需要更多的时间:11+秒。

    似乎不应该这样,因为 runtime.GOMAXPROCS(0) 返回可以同时执行的最大CPU数。

    为什么信号量版本较慢?我如何使其与不安全程序的性能相匹配,同时确保goroutines的数量不会使其崩溃?

    1 回复  |  直到 11 月前
        1
  •  3
  •   Charles Duffy    11 月前

    用你的原始代码,你有一个 线 每个CPU核心,但你的goroutines比线程多。这很正常:Go运行时内部任务在不涉及内核调度器的情况下在goroutines之间切换,在等待I/O时停止一个goroutines并切换到另一个。如果一个任务99.999%在等待网络资源,0.001%在等待CPU,那么一个CPU核一次可以轻松处理1000000个goroutine——你需要足够的内存来分配堆,网络协议需要足够的延迟容忍度,这样如果goroutine需要一些时间来调度(以及如果你的连接需要 相同的 服务器,它需要愿意处理这种负载),但只要你 内存和远程服务(以及中间的网络堆栈)与客户端代码一样健壮,你很好。(HTTP/2支持多路复用,在单个TCP连接上运行无限数量的请求——希望您在这里使用它)。


    当你引入一个只有CPU内核那么多插槽的信号量时 ,你完全击败了这一功能:现在,你不能一次平衡数千个请求(通过处理已准备好的请求并停放未准备好的),而是放慢了代码的速度,只处理与CPU内核数量一样多的请求。 当然 速度较慢;怎么可能不是呢?

        2
  •  0
  •   Source code    11 月前

    Goroutine不是线程或进程。 这接近于一个协程。 因此,即使你使用了数千个goroutine,也没有负担,这远远超过了CPU内核的数量。

    然而,如果你最终使用了太多的goroutines,可能有必要提出它们。 例如,限制对资源的同时访问等。。。

    在这种情况下,建议使用golang.org/x/sync/errgroup包。在内部,使用互斥体控制最大数量。