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

我试过的代码是

链接到游乐场

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

我的问题是:

  • 我如何获得行为?

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


如果您将自己喜欢的网络搜索用于" Go网络搜寻器"(或" golang网络搜寻器")
您会发现许多示例,包括:
进行巡回练习:网络爬虫。
Go中也有一些关于并发的讨论,涵盖了这种情况。

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

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

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

一个简单的解决方案是使用单独的渠道
(例如每个工作人员的in <-chan Job, out chan<- Job)
以及一个读取这些请求的队列/过滤器例程,
将它们附加到一个切片上,该切片要么可以任意增大,要么在全局上进行限制,
并从切片的开头输入另一个通道
(即简单的for-select循环从一个通道读取并写入另一个通道)。
此代码通常还负责跟踪已完成的操作
(例如,访问过的网址的地图),并删除传入的重复请求。

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

在这个简单的示例中,有些事情会被掩盖。
如终止。如果"作业"是一个较大的结构,则您想使用chan *Job[]*Job代替。
在这种情况下,您还需要将地图类型更改为从工作中提取的某些键
(例如Job.URL)
并且您想在list = list[1:]之前执行list[0] = nil来摆脱对*Job指针的引用,并让垃圾回收器更早地对其进行引用。

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

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

例如。:


我使用Go的互斥(Mutex)功能编写了一个解决方案。

当它基于并发运行时,一次限制一个实例访问URL映射可能很重要。 我相信我按照下面的描述实现了它。 请随时尝试。 非常感谢您的反馈,我也将从您的评论中吸取教训。