Go语言圣经 - 第8章 Goroutines 和 Channels - 8.6 示例:并发的Web爬虫

第8章 Goroutines 和 Channels

Go语言中的并发程序可以用两种手段来实现:goroutine 和 channel,其支持顺序通信进程,或被简称为CSP,CSP是一种并发编程模型,在这种并发编程模型中,值会在不同运行实例中传递,第二个手段便是多线程共享内存

8.6 示例:并发的Web爬虫

我们之前做了一个web爬虫,采用的广度优先算法来抓取网页,我们来复原一下这个函数

func crawl(url string) []string {
	fmt.Println(url)
	list,err := links.Extract(url) 
		if err != nil {
			log.Print(err)
		}
	return list	
}

我们看看上述这个程序,它会把抓取的网页放进一个字符串切片中,现在我们把这个切片改为一个带缓存的通道,并且使用goroutine

func main()  {
	worklist := make(chan []string) //定义一个通道

	go func() {worklist <- os.Args[1:]}() //获得一个输入

	seen := make(map[string]bool) //定义一个map

	for list := range worklist {  //遍历获得切片
		for _,link := range list { //遍历获得切片中的link
			if !seen[link] {
				seen[link] = true
				go func(link string) {
					worklist <- crawl(link) //然后把获得的link slice重新装入worklist
				}(link)
			}
		}
	}
}

我们build 并且运行一下,程序是可以高效运行的,

$ go build gopl.io/ch8/crawl1
$ ./crawl1 http://gopl.io/
http://gopl.io/
https://golang.org/help/
https://golang.org/doc/
https://golang.org/blog/
...
2015/07/15 18:22:12 Get ...: dial tcp: lookup blog.golang.org: no such host
2015/07/15 18:22:12 Get ...: dial tcp 23.21.222.120:443: socket: too many open files
...

但是上面的程序有两个问题,第一是它实在太过于并行了,实际上这并不是什么好事,我们当然可以用又缓存的channel来进行限制,但是我们也可以通过改写函数来达到这一目的

var tokens = make(chan struct{},20) //我们定义了一个有缓存的channel

func crawl(url string) []string {
	fmt.Println(url)
	tokens <- struct {}{} //先获取一个token
	list,err := links.Extract(url)
	<- tokens             //再把它丢掉
		if err != nil {
			log.Print(err)
		}
	return list
}

第二个问题是它永远不会终止,我们还得让程序终止。我们需要在worklist为空或者没有crawl的goroutine运行时退出循环,现在我们来改写一下函数

func main()  {
	worklist := make(chan []string) //定义一个通道
	var n int

	n ++

	go func() {worklist <- os.Args[1:]}() //获得一个输入

	seen := make(map[string]bool) //定义一个map

	for ; n>0;n-- {
		list := <-worklist   //遍历获得切片
		for _,link := range list { //遍历获得切片中的link
			if !seen[link] {
				seen[link] = true
				n ++
				go func(link string) {
					worklist <- crawl(link) //然后把获得的link slice重新装入worklist
				}(link)
			}
		}
	}
}

上面这个版本中,计数器n对worklist的发送操作数量进行了限制,每一次只要有元素被发送,我们就会对n进行n++操作, 在for循环这个主goroutine中,,当n减为0时会终止,,这就说命没有活可以干了

现在这个程序的速度是最起初爬虫程序的20倍,并且不会出错以及会在程序执行完结束退出

下面的程序也是避免过度开发的另一种方式,它没有使用计数信号量,而是使用了20个常驻的crawler goroutine,以此来保证最多有20个HTTP请求在并发


func main() {
	worklist := make(chan []string)
	unseenLinks := make(chan string)
	
	go func() {worklist <- os.Args[1:]}()
	
	for i:= 0; i <20; i++ {
		go func() {
			for link := range unseenLinks {
				foundLinks := crawl(link)
				go func() {worklist <- foundLinks}()
			}
		}()
	}
	
	seen := make(map[string]bool)
	for list := range worklist {
		for_,link := range list {
			if !seen[link] {
				seen[link] = true
				unseenLinks <- link
			}
		}
	}
}

所有的爬虫goroutine现在都是被同一个channel - unseenLinks喂饱的了。主goroutine负责拆分它从worklist里拿到的元素,然后把没有抓过的经由unseenLinks channel 发送给一个爬虫的goroutine

seen这个map被限定在main goroutine中,也就是说这个map只能在main goroutine中进行访问

类似于其它的信息隐藏方式,这样的约束可以让我们从一定程度上保证程序的正确性,例如,内部变量不能被外部函数访问,变量没有发生逃逸时外部函数就无法访问变量;一个对象的封装字段无法被该对象的方法以外的方法访问到。在所有的情况下,信息隐藏都可以帮助我们约束我们的程序,使其不发生意料之外的情况

crawl函数爬到的连接在一个专有的goroutine中被发送到worklist中来避免死锁

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值