当前位置: 首页 > 面试题库 >

Go中的Webcrawler

郭鸿信
2023-03-14
问题内容

我正在尝试在Go中构建一个Web搜寻器,我想在其中指定并发工作器的最大数量。只要队列中有要探索的链接,他们都将工作。当队列中的元素少于工作者时,工作者应大喊大叫,但如果发现更多链接,请恢复工作。

我试过的代码是

const max_workers = 6
// simulating links with int
func crawl(wg *sync.WaitGroup, queue chan int) {
    for element := range queue {   
        wg.Done() // why is defer here causing a deadlock?
        fmt.Println("adding 2 new elements ")
        if element%2 == 0 {
            wg.Add(2)
            queue <- (element*100 + 11)
            queue <- (element*100 + 33)
        }

    }
}

func main() {
    var wg sync.WaitGroup
    queue := make(chan int, 10)
    queue <- 0
    queue <- 1
    queue <- 2
    queue <- 3
    var min int
    if (len(queue) < max_workers) {
        min = len(queue)
    } else {
        min = max_workers
    }
    for i := 0; i < min; i++ {
        wg.Add(1)
        go crawl(&wg, queue)
    }
    wg.Wait()
    close(queue)
}

链接到游乐场

这似乎可行,但是有一个陷阱:开始时,我必须用多个元素填充队列。我希望它从(单个)种子页面(在我的示例中queue <- 0)开始,然后动态地增加/缩小工作池。

我的问题是:

  • 我如何获得行为?

  • 为什么延迟会wg.Done()导致死锁?wg.Done()实际完成该功能是否正常?我认为没有defergoroutine不会等待其他部分完成(在解析HTML的实际工作示例中可能会花费更长的时间)。


问题答案:

如果您对“ Go web爬网程序”(或“ golang Web爬网程序”)使用自己喜欢的网络搜索,则会发现许多示例,包括: Go Tour
Exercise:Web
Crawler

。Go中也有一些关于并发的讨论,涵盖了这种情况。

Go中执行此操作的“标准”方法根本不需要涉及等待组。为了回答您的问题之一,defer只有在函数返回时才运行与之排队的事物。您具有运行时间长的功能,因此请勿defer在这样的循环中使用。

“标准”方式是在自己的goroutine中启动想要的许多工人。他们都从同一个频道读取“工作”,如果/无事可做则阻止。完全完成该通道后,它们都会退出。

在诸如履带式的情况下,工人们会发现更多的“工作”要做,并想将他们排队。您不希望他们写回同一通道,因为它只有有限的缓冲量(或没有缓冲!),最终您将阻止所有尝试排队更多工作的工人!

一个简单的解决方案是使用一个单独的通道(例如每个工作人员都有in <-chan Job, out chan<- Job)和一个队列/过滤器goroutine来读取这些请求,将它们附加到一个切片上,该切片要么任意增大要么对其进行全局限制,并且从切片的开头馈入另一个通道(即,从一个通道读取一个简单的for-
select循环并写入另一个通道)。该代码通常还负责跟踪已完成的操作(例如访问的URL映射),并丢弃传入的重复请求。

队列goroutine可能看起来像这样(此处的参数名称过于冗长):

type Job string

func queue(toWorkers chan<- Job, fromWorkers <-chan Job) {
    var list []Job
    done := make(map[Job]bool)
    for {
        var send chan<- Job
        var item Job
        if len(list) > 0 {
            send = toWorkers
            item = list[0]
        }
        select {
        case send <- item:
            // We sent an item, remove it
            list = list[1:]
        case thing := <-fromWorkers:
            // Got a new thing
            if !done[thing] {
                list = append(list, thing)
                done[thing] = true
            }
        }
    }
}

这个简单的示例掩盖了一些内容。如终止。而且,如果“作业”是您要使用的较大结构,chan *Job[]*Job不是。在这种情况下,您还需要将映射类型更改为从作业中提取的某些键(例如, Job.URL也许),并且您想要list[0] = nillist = list[1:]摆脱*Job指针的引用之前先做一下,并让垃圾收集器更早地进行操作。 。

编辑:有关干净终止的一些注意事项。

有几种方法可以像上面那样干净地终止代码。可以使用一个等待组,但是Add /
Done调用的放置需要仔细进行,您可能需要另一个goroutine来进行Wait(然后关闭其中一个通道以开始关闭)。工人不应关闭其输出通道,因为有多个工人,并且您不能多次关闭通道。队列goroutine在不知道何时完成工作的情况下无法告诉何时关闭通往工作人员的通道。

过去,当我使用与上面非常相似的代码时,我在“队列”
goroutine中使用了本地“杰出”计数器(这避免了互斥量或等待组所需的任何同步开销)。将工作发送给工人时,未完成工作的数量会增加。当工人说完成后,它又减少了。我的代码恰好为此提供了另一个渠道(除了要排队的其他节点之外,我的“队列”也在收集结果)。它在自己的通道上可能更干净,但是可以在现有通道上使用特殊值(例如nil
Job指针)。无论如何,有了这样的计数器,本地列表上的现有长度检查只需要查看列表为空并且是时候终止时没有什么未完成的事情。

例如:

    if len(list) > 0 {
        send = toWorkers
        item = list[0]
    } else if outstandingJobs == 0 {
        close(toWorkers)
        return
    }


 类似资料:
  • 问题内容: 您如何在Go中表达“空”值? 我想说 问题答案: 相当于是,因为你已经发现了。但是请注意,您通常不需要在Go中将事物初始化为或为零,因为默认情况下,根据类型(数字为零,引用),所有变量(包括动态分配的变量)都设置为“零值” 。因此,在您的示例中,这样说会导致两个字段都具有Node 。

  • 问题内容: 情况: 我想从控制台获取密码输入- 而不回显用户键入的内容 。有什么可以和Go中的功能媲美吗? 我试过的 我尝试使用,但它回显了键入的内容。 问题答案: 您可以通过执行以下操作来关闭回显,然后在读取密码后将其重新打开

  • 问题内容: 该函数仅采用字符串切片: 但是,能够传递实现函数的任意对象会很好。 Go中是否有类似的东西,还是我必须用ToString方法来修饰现有类型并编写一个包装器? 问题答案: 将方法附加到任何命名类型,并享受任何自定义“ ToString”功能: 游乐场:http://play.golang.org/p/Azql7_pDAA 输出量

  • 问题内容: 是否可以使用Go创建共享库(.so)? 更新 :为此创建了一个“ 问题 ”。 问题答案: 现在可以使用标志 您需要做的是首先运行以下命令: (以上代码使所有通用软件包都可共享!)然后 最后,在编译代码时,您需要运行: 上面这些就是什么,而不是静态链接所有内容而仅动态链接它们,您最终将获得更小的编译文件。为了让您了解我的带有静态链接的“ hello.go”文件为2.3MB,而使用动态链接

  • 问题内容: 我正在看Go,它看起来很有前途。我试图弄清楚如何获得go结构的大小,例如 我当然知道它是24字节,但是我想以编程方式知道它。 您对如何执行此操作有任何想法吗? 问题答案: 注意: OP错误。unsafe.Sizeof确实在示例Coord3d结构上返回24。请参阅下面的评论。

  • 问题内容: 这是我的代码: 输出量 为什么和具有相同的地址?在TCP中,我认为为新连接创建了一个新套接字。 问题答案: 这让我困惑了一秒钟,但这是正确的。确实创建了一个新的套接字(具有唯一的本地+远程地址元组)。维基百科的这段引文很好地描述了它: 服务器可以使用相同的本地端口号和本地IP地址创建多个同时建立的TCP套接字,每个套接字都映射到其自己的服务器子进程,并为自己的客户端进程提供服务。由于远