如何在Golang中尽可能快速地启动Goroutine?

如何在Golang中尽可能快速地启动Goroutine? 我一直在开发一个终端应用程序,需要从1250个URL加载数据。目前我正在使用以下方法:

var wg = sync.WaitGroup
var lock = sync.RWMutex

for _, text := range urls {
    time.Sleep(time.Millisecond * 1)
    wg.Add(1)
    go parseData(text, wg, lock)
}
wg.Wait()

func parseData(url string, wg sync.WaitGroup, lock sync.RWMutex) {
    defer wg.Done()
    defer lock.Unlock()

    htmlBody := readHTML(url)

    lock.Lock()
    addToMap(htmlBody)
}

有没有更好的方法来启动这些Goroutine,让它们能够以连接允许的最快速度执行?如果不使用sleep函数,创建Goroutine的速度太快,会出现"too many connections"错误。

但是,如果我在readHTML函数周围加锁,让它们"同时"启动,获取所有数据的时间就会太长。

我注意到在这种用法中使用time.Sleep()函数时,偶尔HTML仍然会出错。这种情况很少见,大概100次中有1次,但仍然不可取。

欢迎提出任何想法,谢谢!


更多关于如何在Golang中尽可能快速地启动Goroutine?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

感谢 @NobbZ@acim,我会仔细阅读这些解决方案并尝试一下。

#持续学习中 😄

更多关于如何在Golang中尽可能快速地启动Goroutine?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


与使用互斥锁不同,您可能需要使用 sync.Map,而且无论如何都应该采用某种速率限制器,正如 @NobbZ 所说,这样可以避免因 URL 列表持续增长而导致资源过载。

找出你的连接限制并使用工作池。

我的假设是你达到了操作系统的文件描述符限制或者对方因为你的连接速度过快而拒绝连接。


编辑

除了工作池,你也可以使用其他速率限制方法,例如令牌桶实现。

在你的代码中,确实存在几个可以优化的地方,这些优化能帮助你更高效地启动和管理Goroutine,同时避免"too many connections"错误。以下是一些改进建议和示例代码:

  1. 使用带缓冲的通道来控制并发数:通过限制同时运行的Goroutine数量,你可以避免系统资源耗尽,同时保持高并发性能。这比使用time.Sleep更可靠,因为它基于实际完成情况来调度新任务。

  2. 修复WaitGroup和锁的使用:在你的原始代码中,sync.WaitGroupsync.RWMutex应该通过指针传递,否则会导致副本问题,可能引发竞态条件或死锁。

  3. 移除不必要的锁:如果addToMap函数是线程安全的(例如使用sync.Map或内部加锁),或者每个Goroutine处理的数据不冲突,你可能不需要在parseData函数中加锁。如果必须加锁,确保只在必要时加锁。

以下是改进后的代码示例:

package main

import (
    "sync"
)

// 假设urls是你的URL列表
var urls = []string{"http://example.com", "http://example.org"} // 示例URLs

func main() {
    var wg sync.WaitGroup
    // 使用带缓冲的通道限制并发数,例如最大50个并发Goroutine
    maxConcurrency := 50
    semaphore := make(chan struct{}, maxConcurrency)

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            semaphore <- struct{}{} // 获取信号量槽位
            defer func() { <-semaphore }() // 释放信号量槽位

            parseData(u)
        }(url)
    }
    wg.Wait()
}

func parseData(url string) {
    htmlBody := readHTML(url)
    // 假设addToMap是线程安全的,或者使用其他同步机制
    addToMap(htmlBody)
}

// readHTML 模拟从URL读取HTML内容的函数
func readHTML(url string) string {
    // 实现你的HTTP请求逻辑,注意处理错误和设置合理的超时
    // 例如使用http.Get,但实际中建议使用http.Client with timeout
    return "<html>示例内容</html>"
}

// addToMap 将HTML内容添加到映射中,确保线程安全
func addToMap(htmlBody string) {
    // 例如使用sync.Map或加锁来保证并发安全
    // 这里假设已经处理了同步
}

关键改进点:

  • 并发控制:使用带缓冲的通道semaphore作为计数信号量,限制最大并发Goroutine数为50(你可以根据系统资源调整这个值)。这避免了"too many connections"错误,因为同时运行的HTTP请求数量受控。
  • 正确的WaitGroup使用wg.Add(1)在启动Goroutine前调用,并通过闭包捕获url,避免数据竞争。
  • 移除锁:在parseData中移除了sync.RWMutex,假设addToMap是线程安全的。如果addToMap需要同步,可以在其内部实现(例如使用sync.Mutex),而不是在每个Goroutine中加锁,这减少了锁竞争。

为什么这更快且更可靠:

  • 不使用time.Sleep,避免了不必要的延迟,Goroutine在信号量可用时立即启动。
  • 并发数受控,系统资源(如网络连接)不会被耗尽,减少了错误概率。
  • 如果你的readHTML函数使用HTTP客户端,确保设置了合理的超时(例如使用context.WithTimeout),以避免挂起请求。

如果addToMap不是线程安全的,你可以修改它使用sync.Mutex

var mapMutex sync.Mutex
var dataMap = make(map[string]string) // 示例映射

func addToMap(htmlBody string) {
    mapMutex.Lock()
    defer mapMutex.Unlock()
    dataMap[htmlBody] = htmlBody // 示例操作
}

这个方案应该能显著提高性能,同时保持稳定性。根据你的系统网络限制,调整maxConcurrency的值(例如从10开始测试,逐步增加)。

回到顶部